Skip to content

Commit

Permalink
Add support for SWID payload sections
Browse files Browse the repository at this point in the history
This allows us to load the xml and cbor formats for the Intel FSP.
  • Loading branch information
hughsie committed Sep 29, 2023
1 parent 27dc892 commit fb69387
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 162 deletions.
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ XML document:

[uSWID]
tag-id = acbd84ff-9898-4922-8ade-dd4bbe2e40ba
software-name = HughskiColorHug.efi
software-name = HughskiColorHug
software-version = 1.2.3
version-scheme = multipartnumeric
product = ColorHug
Expand Down Expand Up @@ -124,16 +124,13 @@ using multiple files on `--load`) then you can specify the correct identity usin
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 a file hash:

[uSWID-Hash]
value = 5525fbd0911b8dcbdc6f0c081ac27fd55b75d6d261c62fa05b9bdc0b72b481f6
[uSWID-Payload]
name = HughskiColorHug.efi
size = 20480
hash = 5525fbd0911b8dcbdc6f0c081ac27fd55b75d6d261c62fa05b9bdc0b72b481f6

Additional hashes can also be provided:

[uSWID-Hash:SHA512]
value = 50db094fc160e758e5cc14f47688ce3d862ab2f3ced91b97e89ef7702293cbca…

Adding Deps
-----------
Adding Links
------------

Dependancies like compilers or other security-relevant libraries can be added using:

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"cbor2",
"lxml",
"pefile",
"importlib-metadata >= 1.0 ; python_version < '3.8'"
"importlib-metadata >= 1.0 ; python_version < '3.8'",
],
entry_points={
"console_scripts": [
Expand Down Expand Up @@ -59,6 +59,7 @@
"format_swid.pyi",
"format_uswid.pyi",
"hash.pyi",
"payload.pyi",
"identity.pyi",
"link.pyi",
"__init__.pyi",
Expand Down
1 change: 1 addition & 0 deletions uswid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from uswid.container import uSwidContainer
from uswid.link import uSwidLink, uSwidLinkRel
from uswid.hash import uSwidHash, uSwidHashAlg
from uswid.payload import uSwidPayload
from uswid.identity import uSwidIdentity
from uswid.entity import uSwidEntity, uSwidEntityRole
from uswid.enums import uSwidGlobalMap, uSwidVersionScheme, USWID_HEADER_MAGIC
Expand Down
5 changes: 2 additions & 3 deletions uswid/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,5 @@ def __init__(
self.roles.extend(roles)

def __repr__(self) -> str:
return "uSwidEntity({},{}->{})".format(
self.name, self.regid, ",".join([role.name for role in self.roles])
)
role_str = ",".join([role.name for role in self.roles])
return f'uSwidEntity(regid="{self.regid}",name="{self.name}",roles={role_str})'
133 changes: 100 additions & 33 deletions 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, Tuple
from typing import Dict, Any, Optional

import io
import uuid
Expand All @@ -21,6 +21,20 @@
from .entity import uSwidEntity, uSwidEntityRole
from .link import uSwidLink, uSwidLinkRel
from .hash import uSwidHash, uSwidHashAlg
from .payload import uSwidPayload


def _get_one_or_more(data: Dict[uSwidGlobalMap, Any], key: uSwidGlobalMap) -> list[Any]:
value = data.get(key, [])
if isinstance(value, dict):
return [value]
return value


def _set_one_or_more(
data: Dict[uSwidGlobalMap, Any], key: uSwidGlobalMap, value: list[Any]
) -> None:
data[key] = value if len(value) > 1 else value[0]


class uSwidFormatCoswid(uSwidFormatBase):
Expand Down Expand Up @@ -67,9 +81,24 @@ 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]:
def _save_hash(self, ihash: uSwidHash) -> list[int, bytes]:
"""exports a uSwidHash CoSWID section"""
return (ihash.alg_id, bytes.fromhex(ihash.value))
return [ihash.alg_id, bytes.fromhex(ihash.value)]

def _save_payload(self, payload: uSwidPayload) -> Dict[uSwidGlobalMap, Any]:
"""exports a uSwidPayload CoSWID section"""

data: Dict[uSwidGlobalMap, Any] = {}
if payload.name:
data[uSwidGlobalMap.FS_NAME] = payload.name
if payload.size:
data[uSwidGlobalMap.SIZE] = payload.size
if payload.hashes:
payload_hashes = []
for ihash in payload.hashes:
payload_hashes.append(self._save_hash(ihash))
_set_one_or_more(data, uSwidGlobalMap.HASH, payload_hashes)
return {uSwidGlobalMap.FILE: data}

def _save_entity(self, entity: uSwidEntity) -> Dict[uSwidGlobalMap, Any]:
"""exports a uSwidEntity CoSWID section"""
Expand All @@ -78,10 +107,7 @@ def _save_entity(self, entity: uSwidEntity) -> Dict[uSwidGlobalMap, Any]:
data[uSwidGlobalMap.ENTITY_NAME] = entity.name
if entity.regid:
data[uSwidGlobalMap.REG_ID] = entity.regid
if len(entity.roles) == 1:
data[uSwidGlobalMap.ROLE] = entity.roles[0]
else:
data[uSwidGlobalMap.ROLE] = entity.roles
_set_one_or_more(data, uSwidGlobalMap.ROLE, entity.roles)
return data

def _save_identity(self, identity: uSwidIdentity) -> bytes:
Expand Down Expand Up @@ -129,10 +155,13 @@ 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
]
# payloads
if identity.payloads:
_set_one_or_more(
data,
uSwidGlobalMap.PAYLOAD,
[self._save_payload(payload) for payload in identity.payloads],
)

# entities
if not identity._entities:
Expand All @@ -149,12 +178,16 @@ def _save_identity(self, identity: uSwidIdentity) -> bytes:
has_tag_creator = True
if not has_tag_creator:
raise NotSupportedError("all entries MUST have a tag-creator")
data[uSwidGlobalMap.ENTITY] = [
self._save_entity(entity) for entity in identity._entities.values()
]
data[uSwidGlobalMap.LINK] = [
self._save_link(link) for link in identity._links.values()
]
_set_one_or_more(
data,
uSwidGlobalMap.ENTITY,
[self._save_entity(entity) for entity in identity._entities.values()],
)
_set_one_or_more(
data,
uSwidGlobalMap.LINK,
[self._save_link(link) for link in identity._links.values()],
)
return cbor2.dumps(data)

def _load_link(self, link: uSwidLink, data: Dict[uSwidGlobalMap, Any]) -> None:
Expand All @@ -167,6 +200,8 @@ def _load_link(self, link: uSwidLink, data: Dict[uSwidGlobalMap, Any]) -> None:
rel_data = data.get(uSwidGlobalMap.REL)
if isinstance(rel_data, str):
link.rel = rel_data
if isinstance(rel_data, int):
rel_data = uSwidLinkRel(rel_data)
if isinstance(rel_data, uSwidLinkRel):
LINK_MAP: Dict[uSwidLinkRel, str] = {
uSwidLinkRel.LICENSE: "license",
Expand All @@ -192,10 +227,32 @@ def _load_link(self, link: uSwidLink, data: Dict[uSwidGlobalMap, Any]) -> None:
)
) from e

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

def _load_payload(
self,
payload: uSwidPayload,
data: Dict[uSwidGlobalMap, Any],
) -> None:
"""imports a uSwidPayload CoSWID section"""
for key, value in data.items():
if key == uSwidGlobalMap.FS_NAME:
payload.name = value
if key == uSwidGlobalMap.SIZE:
payload.size = value
if key == uSwidGlobalMap.HASH:
if not isinstance(value[0], list):
value = [value]
for hash_data in value:
ihash = uSwidHash()
self._load_hash(ihash, hash_data)
payload.add_hash(ihash)

def _load_entity(
self,
Expand Down Expand Up @@ -231,6 +288,12 @@ def _load_identity(
except EOFError as e:
raise NotSupportedError("invalid header") from e

# strip off digital signature
if isinstance(data, cbor2.CBORTag):
if data.tag != 98:
raise NotSupportedError("invalid digital signature")
data = cbor2.loads(data.value[2])

# identity can be specified as a string or in binary
tag_id_bytes = data.get(uSwidGlobalMap.TAG_ID, None)
if isinstance(tag_id_bytes, str):
Expand All @@ -247,10 +310,7 @@ def _load_identity(
identity.version_scheme = data.get(uSwidGlobalMap.VERSION_SCHEME, None)

# optional metadata
software_metas = data.get(uSwidGlobalMap.SOFTWARE_META, [])
if isinstance(software_metas, dict):
software_metas = [software_metas]
for sm in software_metas:
for sm in _get_one_or_more(data, uSwidGlobalMap.SOFTWARE_META):
for key, value in sm.items():
if key == uSwidGlobalMap.GENERATOR:
identity.generator = value
Expand All @@ -267,17 +327,24 @@ 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)
# payload
file_datas = []
for payload_data in _get_one_or_more(data, uSwidGlobalMap.PAYLOAD):
file_datas.extend(_get_one_or_more(payload_data, uSwidGlobalMap.FILE))
for directory_data in _get_one_or_more(
payload_data, uSwidGlobalMap.DIRECTORY
):
for path_data in _get_one_or_more(
directory_data, uSwidGlobalMap.PATH_ELEMENTS
):
file_datas.extend(_get_one_or_more(path_data, uSwidGlobalMap.FILE))
for file_data in file_datas:
payload = uSwidPayload()
self._load_payload(payload, file_data)
identity.add_payload(payload)

# entities
entities = data.get(uSwidGlobalMap.ENTITY, [])
if isinstance(entities, dict):
entities = [entities]
for entity_data in entities:
for entity_data in _get_one_or_more(data, uSwidGlobalMap.ENTITY):
entity = uSwidEntity()
self._load_entity(entity, entity_data)
# skip invalid entries
Expand All @@ -286,7 +353,7 @@ def _load_identity(
identity.add_entity(entity)

# links
for link_data in data.get(uSwidGlobalMap.LINK, []):
for link_data in _get_one_or_more(data, uSwidGlobalMap.LINK):
link = uSwidLink()
self._load_link(link, link_data)
identity.add_link(link)
Expand Down
33 changes: 19 additions & 14 deletions uswid/format_goswid.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .entity import uSwidEntity
from .link import uSwidLink
from .hash import uSwidHash, uSwidHashAlg
from .payload import uSwidPayload
from .format_swid import _ENTITY_MAP_FROM_XML, _ENTITY_MAP_TO_XML


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

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

node: Dict[str, str] = {}
Expand Down Expand Up @@ -134,10 +135,10 @@ def _save_identity_internal(self, identity: uSwidIdentity) -> Dict[str, Any]:
root["software-meta"] = [node]

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

# entities
if identity.entities:
Expand Down Expand Up @@ -167,16 +168,20 @@ def _load_link(self, link: uSwidLink, node: Dict[str, str]) -> None:
link.rel = node.get("rel")

def _load_hash(self, ihash: uSwidHash, node: Dict[str, str]) -> None:
"""imports a uSwidLink goSWID section"""
"""imports a uSwidHash 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"""
def _load_payload(self, payload: uSwidPayload, node: Dict[str, str]) -> None:
"""imports a uSwidPayload goSWID section"""

ihash.alg_id = node.get("alg_id")
ihash.value = node.get("value")
payload.name = node.get("name")
payload.size = node.get("size")
for node_hash in node.get("hash", []):
ihash = uSwidHash()
self._load_hash(ihash, node_hash)
payload.add_hash(ihash)

def _load_entity(
self,
Expand Down Expand Up @@ -245,12 +250,12 @@ def _load_identity_internal(
except KeyError:
pass

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

Expand Down
Loading

0 comments on commit fb69387

Please sign in to comment.