sbom: drop liboqs dependency support (removed from wolfSSL)

wolfSSL removed liboqs: Falcon is now provided natively by wolfCrypt, and
--with-liboqs is a deprecated no-op (configure.ac). A build therefore no
longer links liboqs, so recording it as an SBOM dependency is dead code and
the SBOM integration CI (which asserted a liboqs dep package) failed.

Remove the liboqs dependency throughout:
- scripts/gen-sbom: drop DEP_META['liboqs'] and the --dep-liboqs flag.
- Makefile.am / configure.ac: drop --dep-liboqs "$(ENABLED_LIBOQS)" and the
  now-unused AC_SUBST([ENABLED_LIBOQS]).
- .github/workflows/sbom.yml: drop the liboqs install / --with-liboqs steps
  and the liboqs dep assertion; keep the native-Falcon build so the
  HAVE_FALCON build-property capture is still exercised.
- scripts/test_gen_sbom.py: drop the liboqs-specific tests, guard against
  the key reappearing, and use openssl as the example dep elsewhere.
- doc/SBOM.md: drop the --dep-liboqs / liboqs dependency references.
This commit is contained in:
Mark Atwood
2026-07-23 22:32:50 -07:00
parent c6a336f75f
commit be9963f392
6 changed files with 56 additions and 168 deletions
+8 -90
View File
@@ -655,102 +655,20 @@ jobs:
print('simple SPDX override: ok')
PY
# ---- liboqs / Falcon dep entry ---------------------------------------
# Without this, every code path that emits a dep package - pkg-config
# lookup, supplier/purl/license construction, deterministic UUID
# derivation for deps - is uncovered by CI. A future rename or shape
# break in DEP_META['liboqs'] would silently land.
# ---- Falcon (native) build-property coverage --------------------------
# liboqs was removed from wolfSSL (Falcon is now provided natively by
# wolfCrypt), so there is no external dependency package to record; this
# exercises the SBOM build-property capture with an algorithm enabled.
- name: Install liboqs (provides liboqs.pc for --with-liboqs)
# Ubuntu noble (24.04) does not ship liboqs-dev in its archive
# (Debian sid has 0.7.x; Ubuntu only has unsupported PPAs). Build
# from a pinned upstream commit so this job stays deterministic across
# runs - any future liboqs API/ABI break shows up here, not in
# production builds. Pinning matters: SBOM correctness assertions
# below check purl shape, and an unpinned 'main' would silently
# change what pkg-config reports as the version string.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake ninja-build libssl-dev
# Pin to the exact commit the 0.12.0 tag resolves to (a tag is
# mutable, a SHA is not) so this provenance workflow's own build
# inputs are immutable. --filter=blob:none keeps the commit graph
# so `git checkout <SHA>` works without a full blob download.
git clone --filter=blob:none \
https://github.com/open-quantum-safe/liboqs /tmp/liboqs
git -C /tmp/liboqs checkout f4b96220e4bd208895172acc4fedb5a191d9f5b1 # 0.12.0
# -DOQS_USE_OPENSSL=OFF is load-bearing: without it, liboqs's
# installed common.h pulls <openssl/crypto.h> (system) into every
# TU that includes <oqs/oqs.h>. wolfssl/wolfcrypt/falcon.h
# includes <oqs/oqs.h>, so once --enable-falcon is on, every
# wolfSSL TU that pulls falcon.h also pulls system OpenSSL,
# which collides with wolfssl/openssl/ssl.h under -Werror
# (CRYPTO_UNLOCK, sk_num, OPENSSL_malloc_init, ... all redefined).
# OFF makes liboqs use its bundled SHA/randombytes (the #else
# branches in oqs/common.h), keeping the build hermetic.
cmake -S /tmp/liboqs -B /tmp/liboqs/build -GNinja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DBUILD_SHARED_LIBS=ON \
-DOQS_BUILD_ONLY_LIB=ON \
-DOQS_DIST_BUILD=OFF \
-DOQS_USE_OPENSSL=OFF
cmake --build /tmp/liboqs/build --parallel "$(nproc)"
sudo cmake --install /tmp/liboqs/build
sudo ldconfig
# /usr/local/lib/pkgconfig is on pkg-config's compiled-in path
# on Ubuntu, but export via $GITHUB_ENV so a future image change
# cannot silently break --with-liboqs autodetection. ${VAR:+:$VAR}
# avoids a trailing colon when PKG_CONFIG_PATH is unset.
echo "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" \
>> "$GITHUB_ENV"
- name: Verify liboqs.pc visible to pkg-config
# Separate step so the $GITHUB_ENV write above has taken effect
# for this shell; an in-step call would only be exercising the
# compiled-in default path, not the export.
run: pkg-config --modversion liboqs
- name: Configure with --with-liboqs --enable-falcon
- name: Configure with --enable-falcon (native)
run: |
make distclean
autoreconf -ivf
./configure --enable-shared --enable-experimental \
--with-liboqs --enable-falcon
./configure --enable-shared --enable-experimental --enable-falcon
- name: Build + generate SBOM with liboqs enabled
- name: Build + generate SBOM with Falcon enabled
run: make sbom
- name: liboqs dep entry resolves to a CVE-trackable identifier
# The point of recording liboqs (rather than `falcon`) is that
# OSV / Grype / Trivy / Dependency-Track key vulnerability
# records off purl + name. These assertions guard the contract
# that pulled the entry away from the algorithm name.
run: |
python3 - <<'PY'
import glob, json, re, sys
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
d = json.load(f)
pkgs = {p['name']: p for p in d['packages']}
assert 'liboqs' in pkgs, list(pkgs)
assert 'falcon' not in pkgs, "algorithm name leaked as a dep package"
liboqs = pkgs['liboqs']
assert liboqs['supplier'] == 'Organization: Open Quantum Safe', \
liboqs['supplier']
refs = {r['referenceType']: r['referenceLocator']
for r in liboqs.get('externalRefs', [])}
assert 'purl' in refs, refs
assert re.match(r'pkg:github/open-quantum-safe/liboqs@', refs['purl']), \
refs['purl']
# CycloneDX side: same package + version present.
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
cdx = json.load(f)
deps = {c['name']: c for c in cdx.get('components', [])}
assert 'liboqs' in deps, list(deps)
print('liboqs dep entry: ok ->', refs['purl'])
PY
- name: HAVE_FALCON algorithm flag is captured as a build property
# Algorithm visibility moved out of the dep entry; this verifies
# it is still preserved (just somewhere honest).
@@ -811,7 +729,7 @@ jobs:
# a downstream packager, or the next maintainer triaging a regression
# can download them straight from the run summary instead of replaying
# the full job locally. `if: always()` so a failed assertion above
# (license matrix, NTIA, CDX schema, liboqs dep entry, ...) still ships
# (license matrix, NTIA, CDX schema, Falcon build prop, ...) still ships
# the bytes it failed on. The last `make sbom` invocation in this job
# is the simple SPDX override step, but the path matches every wolfssl
# SPDX/CDX in $PWD - if any are present at job end they will be picked
+2 -3
View File
@@ -499,12 +499,12 @@ WOLFSSL_LIB_DSO_BASENAMES = \
# https://example.com/sbom/wolfssl-X.Y.Z.spdx.json).
# SBOM_DEP_VERSIONS Space-separated KEY=VERSION list forwarded to
# gen-sbom as repeated --dep-version flags (KEY is
# one of the known deps, e.g. liboqs / libz). Use
# one of the known deps, e.g. libz / openssl). Use
# this on build/packaging hosts that lack the dep's
# pkg-config .pc file, where version detection would
# otherwise fall back to NOASSERTION (SPDX) / an
# omitted version+purl (CycloneDX). Example:
# make sbom SBOM_DEP_VERSIONS='liboqs=0.10.0 libz=1.3.1'.
# make sbom SBOM_DEP_VERSIONS='libz=1.3.1 openssl=3.5.0'.
# SBOM_LIB_OVERRIDE Absolute path to the library artefact whose
# SHA-256 should land in the SBOM, INSTEAD of
# discovering one via a private staging install.
@@ -581,7 +581,6 @@ sbom:
--options-h $(abs_builddir)/wolfssl/options.h \
--lib "$$sbom_lib" \
--dep-libz "$(ENABLED_LIBZ)" \
--dep-liboqs "$(ENABLED_LIBOQS)" \
$(foreach dv,$(SBOM_DEP_VERSIONS),--dep-version '$(dv)') \
--cdx-out $(abs_builddir)/$(SBOM_CDX) \
--spdx-out $(abs_builddir)/$(SBOM_SPDX); \
-1
View File
@@ -13183,7 +13183,6 @@ AC_PATH_PROG([PYTHON3], [python3])
AC_PATH_PROG([PYSPDXTOOLS], [pyspdxtools])
AC_PATH_PROG([GIT], [git])
AC_SUBST([ENABLED_LIBZ])
AC_SUBST([ENABLED_LIBOQS])
# Bomsh (OmniBOR build artifact tracing + SBOM enrichment)
AC_PATH_PROG([BOMTRACE3], [bomtrace3])
+10 -11
View File
@@ -81,7 +81,6 @@ Optional flags:
|---|---|
| `--supplier "Acme Inc."` | Override the default `wolfSSL Inc.` (rare) |
| `--dep-libz yes` | If your build links `libz` |
| `--dep-liboqs yes` | If your build links `liboqs` |
| `--dep-version libz=1.3.1` | Explicit dep version when `pkg-config` is unavailable (typical cross-compile) |
| `--license-override LicenseRef-wolfSSL-Commercial` | If you are a commercial licensee, not GPL |
| `--license-text /path/to/commercial-license.txt` | Required when `--license-override` is a `LicenseRef-*` |
@@ -458,7 +457,7 @@ Both formats contain the same information:
| CPE | `cpe:2.3:a:wolfssl:wolfssl:<version>:*:*:*:*:*:*:*` |
| PURL | `pkg:github/wolfSSL/wolfssl@v<version>` (resolves directly in OSV / GHSA / Snyk / Trivy without per-vendor mapping) |
| Download location | `https://github.com/wolfSSL/wolfssl` |
| Third-party deps | none in a default build; `--with-libz` adds zlib and `--with-liboqs` adds liboqs (recorded as `DEPENDS_ON` packages with their own purl/CPE/supplier). All builds depend transitively on the host C runtime; this is not enumerated as an SBOM component since it is system-supplied and varies per runtime target. |
| Third-party deps | none in a default build; `--with-libz` adds zlib (recorded as a `DEPENDS_ON` package with its own purl/CPE/supplier). All builds depend transitively on the host C runtime; this is not enumerated as an SBOM component since it is system-supplied and varies per runtime target. |
#### License detection
@@ -519,14 +518,14 @@ make sbom \
#### External dependency version detection
The optional external dependencies wolfSSL can link against (`libz` and
`liboqs`) are both installed packages and are queried via
`pkg-config --modversion` at SBOM generation time. The SBOM records each
linked library by its package name (`zlib`, `liboqs`) so that downstream
vulnerability scanners (OSV, Grype, Trivy, Dependency-Track) match CVEs
against the right component. Algorithm enablement (e.g. Falcon, which is
reachable only via liboqs) is captured separately as build properties
(`wolfssl:build:HAVE_FALCON` etc.) parsed from `wolfssl/options.h`.
The optional external dependency wolfSSL can link against (`libz`) is an
installed package and is queried via `pkg-config --modversion` at SBOM
generation time. The SBOM records the linked library by its package name
(`zlib`) so that downstream vulnerability scanners (OSV, Grype, Trivy,
Dependency-Track) match CVEs against the right component. Algorithm
enablement (e.g. Falcon, now provided natively by wolfCrypt) is captured
separately as build properties (`wolfssl:build:HAVE_FALCON` etc.) parsed
from `wolfssl/options.h`.
If pkg-config does not report a version (the package is not installed, or
its `.pc` file is missing):
@@ -543,7 +542,7 @@ that lacks the dependency's `.pc` file can still record the version instead
of `NOASSERTION`:
```sh
make sbom SBOM_DEP_VERSIONS='liboqs=0.10.0 libz=1.3.1'
make sbom SBOM_DEP_VERSIONS='libz=1.3.1'
```
### 2.5 Validating the SBOM manually
+1 -19
View File
@@ -122,19 +122,6 @@ DEP_META = {
'pkgconfig': 'wolfssl',
'purl': lambda v: f'pkg:github/wolfSSL/wolfssl@v{v}',
},
# liboqs is the only PQ external dependency wolfSSL still links against
# after upstream PR #10293 collapsed the rest of the PQ surface into
# native wolfCrypt. Today, --enable-falcon strictly implies --with-liboqs
# (configure.ac enforces both directions), so a build that links liboqs
# is precisely a build that exposed Falcon.
'liboqs': {
'name': 'liboqs',
'supplier': 'Open Quantum Safe',
'license': 'MIT',
'download': 'https://github.com/open-quantum-safe/liboqs',
'pkgconfig': 'liboqs',
'purl': lambda v: f'pkg:github/open-quantum-safe/liboqs@{v}',
},
'libz': {
'name': 'zlib',
'supplier': 'Jean-loup Gailly and Mark Adler',
@@ -1078,7 +1065,7 @@ def _resolve_dep_versions(enabled_deps, overrides):
value instead of each re-invoking pkg-config. Caching the result
(including None) means a later dep_version() lookup short-circuits on the
membership check rather than re-shelling to `pkg-config --modversion`, so
a default --with-libz --with-liboqs build calls pkg-config once per dep
a default --with-libz build calls pkg-config once per dep
(not once per dep per output format) and the two documents can never
disagree if pkg-config output were ever non-deterministic."""
for key in enabled_deps:
@@ -1185,10 +1172,6 @@ def main():
'openssl.pc.')
parser.add_argument('--dep-libz', default='no',
help='yes if built with --with-libz')
parser.add_argument('--dep-liboqs', default='no',
help='yes if built with --with-liboqs (the package '
'wolfSSL links against; --enable-falcon implies '
'this in any legal configuration)')
parser.add_argument('--dep-version', action='append', default=[],
metavar='KEY=VERSION',
help='Override pkg-config version detection for a '
@@ -1257,7 +1240,6 @@ def main():
('wolfssl', args.dep_wolfssl),
('openssl', args.dep_openssl),
('libz', args.dep_libz),
('liboqs', args.dep_liboqs),
]
if flag.lower() == 'yes'
]
+35 -44
View File
@@ -830,9 +830,9 @@ class TestDepMetaShape(unittest.TestCase):
# wolfssl is tracked so downstream wolfSSL-stack products (wolfSSH,
# wolfMQTT, ...) can declare it via --dep-wolfssl; openssl so the
# OpenSSL-compat products (wolfProvider, wolfEngine) can declare it via
# --dep-openssl; libz/liboqs are wolfSSL's own optional linked deps.
# --dep-openssl; libz is wolfSSL's own optional linked dep.
self.assertEqual(set(gs.DEP_META.keys()),
{'wolfssl', 'openssl', 'libz', 'liboqs'})
{'wolfssl', 'openssl', 'libz'})
def test_wolfssl_dep_entry_describes_the_linked_artefact(self):
wolfssl = gs.DEP_META['wolfssl']
@@ -861,22 +861,14 @@ class TestDepMetaShape(unittest.TestCase):
openssl['purl']('3.5.0'),
'pkg:github/openssl/openssl@openssl-3.5.0')
def test_liboqs_entry_describes_the_linked_artefact(self):
liboqs = gs.DEP_META['liboqs']
self.assertEqual(liboqs['name'], 'liboqs')
self.assertEqual(liboqs['supplier'], 'Open Quantum Safe')
self.assertEqual(liboqs['pkgconfig'], 'liboqs')
self.assertEqual(
liboqs['purl']('0.10.0'),
'pkg:github/open-quantum-safe/liboqs@0.10.0')
def test_no_stale_dep_keys(self):
# `falcon` is an algorithm, not a linked package; it must not
# appear as a dep entry (algorithm enablement lives in
# build_props parsed from options.h). `libxmss` and `liblms`
# were removed upstream; their re-appearance here would
# silently emit unresolvable identifiers in the SBOM.
for stale in ('falcon', 'libxmss', 'liblms', 'xmss', 'lms'):
# build_props parsed from options.h). `liboqs`, `libxmss` and
# `liblms` were removed upstream (Falcon is now native wolfCrypt,
# no liboqs); their re-appearance here would silently emit
# unresolvable identifiers in the SBOM.
for stale in ('falcon', 'liboqs', 'libxmss', 'liblms', 'xmss', 'lms'):
self.assertNotIn(stale, gs.DEP_META)
@@ -895,22 +887,21 @@ class TestEnabledDepsCli(unittest.TestCase):
capture_output=True, text=True
)
def test_dep_liboqs_is_accepted(self):
def test_dep_flags_are_accepted(self):
result = self._run('--help')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn('--dep-liboqs', result.stdout)
self.assertIn('--dep-libz', result.stdout)
self.assertIn('--dep-wolfssl', result.stdout)
self.assertIn('--dep-openssl', result.stdout)
def test_removed_flags_are_rejected(self):
# Each of these was either renamed (--dep-falcon -> --dep-liboqs)
# or removed entirely (--dep-libxmss/--dep-liblms with upstream
# removal of the libraries). argparse should reject them as
# unrecognised, not silently accept them. We pass the full set
# of required args (against /dev/null sentinels) so argparse
# progresses to the unknown-flag check; we never want
# gen-sbom to actually generate anything in this test.
# Each of these was removed: --dep-falcon/--dep-libxmss/--dep-liblms
# (the libraries were removed upstream) and --dep-liboqs (liboqs support
# was removed; Falcon is now provided natively by wolfCrypt). argparse
# should reject them as unrecognised, not silently accept them. We pass
# the full set of required args (against /dev/null sentinels) so argparse
# progresses to the unknown-flag check; we never want gen-sbom to
# actually generate anything in this test.
required = [
'--name', 'wolfssl',
'--version', '0.0.0-test',
@@ -920,9 +911,9 @@ class TestEnabledDepsCli(unittest.TestCase):
'--cdx-out', '/dev/null',
'--spdx-out', '/dev/null',
]
for stale_flag in ('--dep-falcon', '--dep-libxmss', '--dep-liblms',
'--dep-libxmss-root', '--dep-liblms-root',
'--git'):
for stale_flag in ('--dep-falcon', '--dep-liboqs', '--dep-libxmss',
'--dep-liblms', '--dep-libxmss-root',
'--dep-liblms-root', '--git'):
result = self._run(*required, stale_flag, 'no')
self.assertNotEqual(result.returncode, 0,
f"{stale_flag!r} unexpectedly accepted")
@@ -1313,7 +1304,7 @@ class TestDepVersionOverride(unittest.TestCase):
self.assertEqual(gs.dep_version('libz'), '1.0.0')
self.assertEqual(gs.dep_version('libz', {}), '1.0.0')
self.assertEqual(
gs.dep_version('libz', {'liboqs': '0.0'}), '1.0.0')
gs.dep_version('libz', {'openssl': '0.0'}), '1.0.0')
finally:
gs.pkgconfig_version = original
@@ -1327,16 +1318,16 @@ class TestDepVersionOverride(unittest.TestCase):
def test_parse_overrides_accepts_known_keys(self):
out = gs._parse_dep_version_overrides([
'libz=1.3.1', 'liboqs=0.10.0',
'libz=1.3.1', 'openssl=3.5.0',
])
self.assertEqual(out, {'libz': '1.3.1', 'liboqs': '0.10.0'})
self.assertEqual(out, {'libz': '1.3.1', 'openssl': '3.5.0'})
class TestResolveDepVersionsSingleShot(unittest.TestCase):
"""Each enabled dependency's version must be resolved exactly once (in
main, via _resolve_dep_versions), not once per output format. Without
the precompute, generate_cdx and generate_spdx each call dep_version()
independently, so a default --with-libz --with-liboqs build would shell
independently, so a build linking libz + openssl would shell
out to `pkg-config --modversion` four times (2 deps x CDX+SPDX) instead
of twice -- and the two documents could disagree if pkg-config were ever
non-deterministic. These tests lock that single-resolution behaviour in."""
@@ -1346,14 +1337,14 @@ class TestResolveDepVersionsSingleShot(unittest.TestCase):
original = gs.pkgconfig_version
try:
gs.pkgconfig_version = lambda pkg: (calls.append(pkg), '1.2.3')[1]
overrides = gs._resolve_dep_versions(['libz', 'liboqs'], {})
overrides = gs._resolve_dep_versions(['libz', 'openssl'], {})
self.assertEqual(len(calls), 2)
self.assertEqual(overrides['libz'], '1.2.3')
self.assertEqual(overrides['liboqs'], '1.2.3')
self.assertEqual(overrides['openssl'], '1.2.3')
# The emitters reuse the cached value: a later dep_version() for
# an already-resolved key must not re-invoke pkg-config.
gs.dep_version('libz', overrides)
gs.dep_version('liboqs', overrides)
gs.dep_version('openssl', overrides)
self.assertEqual(len(calls), 2)
finally:
gs.pkgconfig_version = original
@@ -1374,11 +1365,11 @@ class TestResolveDepVersionsSingleShot(unittest.TestCase):
original = gs.pkgconfig_version
try:
gs.pkgconfig_version = lambda pkg: (calls.append(pkg), None)[1]
overrides = gs._resolve_dep_versions(['liboqs'], {})
self.assertIn('liboqs', overrides)
self.assertIsNone(overrides['liboqs'])
overrides = gs._resolve_dep_versions(['openssl'], {})
self.assertIn('openssl', overrides)
self.assertIsNone(overrides['openssl'])
# A cached None must short-circuit later lookups too.
gs.dep_version('liboqs', overrides)
gs.dep_version('openssl', overrides)
self.assertEqual(len(calls), 1)
finally:
gs.pkgconfig_version = original
@@ -1740,7 +1731,7 @@ class TestCdxDepComponent(unittest.TestCase):
def test_returns_bomref_and_component(self):
# Stub pkgconfig_version so the test does not depend on the
# build host having libz / liboqs installed.
# build host having libz / openssl installed.
original = gs.pkgconfig_version
try:
gs.pkgconfig_version = lambda *_a, **_k: '1.3.1'
@@ -1812,7 +1803,7 @@ class TestSpdxDepPackage(unittest.TestCase):
original = gs.pkgconfig_version
try:
gs.pkgconfig_version = lambda *_a, **_k: '0.10.0'
spdx_id, pkg = gs.spdx_dep_package('liboqs')
spdx_id, pkg = gs.spdx_dep_package('openssl')
finally:
gs.pkgconfig_version = original
self.assertTrue(spdx_id.startswith('SPDXRef-Package-'))
@@ -1825,7 +1816,7 @@ class TestSpdxDepPackage(unittest.TestCase):
_re.match(r'\ASPDXRef-[A-Za-z0-9.-]+\Z', spdx_id),
f'invalid SPDXID shape: {spdx_id!r}')
self.assertEqual(pkg['SPDXID'], spdx_id)
self.assertEqual(pkg['name'], 'liboqs')
self.assertEqual(pkg['name'], 'openssl')
self.assertEqual(pkg['versionInfo'], '0.10.0')
self.assertEqual(pkg['filesAnalyzed'], False)
# Both license fields must agree; SPDX validators accept
@@ -1840,7 +1831,7 @@ class TestSpdxDepPackage(unittest.TestCase):
original = gs.pkgconfig_version
try:
gs.pkgconfig_version = lambda *_a, **_k: None
_, pkg = gs.spdx_dep_package('liboqs')
_, pkg = gs.spdx_dep_package('openssl')
finally:
gs.pkgconfig_version = original
self.assertEqual(pkg['versionInfo'], 'NOASSERTION')
@@ -1853,7 +1844,7 @@ class TestSpdxDepPackage(unittest.TestCase):
original = gs.pkgconfig_version
try:
gs.pkgconfig_version = lambda *_a, **_k: '0.10.0'
_, pkg = gs.spdx_dep_package('liboqs')
_, pkg = gs.spdx_dep_package('openssl')
finally:
gs.pkgconfig_version = original
purl_refs = [
@@ -1861,7 +1852,7 @@ class TestSpdxDepPackage(unittest.TestCase):
if r.get('referenceType') == 'purl'
]
self.assertEqual(len(purl_refs), 1)
self.assertIn('liboqs', purl_refs[0]['referenceLocator'])
self.assertIn('openssl', purl_refs[0]['referenceLocator'])
self.assertIn('0.10.0', purl_refs[0]['referenceLocator'])