A Swift library for zero-knowledge proof (ZKP) generation and verification of mdoc (mobile document) credentials using the Longfellow ZK system. Built on top of Google's MdocZK native library, it enables privacy-preserving selective disclosure of ISO/IEC 18013-5 mobile documents (e.g., mDL — mobile driving licence) and EU Digital Identity Wallet Age Verification credentials.
LongfellowZkp provides a Swift-friendly interface to the native Longfellow ZK prover and verifier. It allows a holder to generate a zero-knowledge proof that certain attributes in their mdoc are valid (e.g., age_over_18 == true) without revealing the full document, and allows a verifier to confirm the proof.
- Proof generation — produce ZK proofs from mdoc
DeviceResponseCBOR data - Proof verification — verify proofs against issuer public keys and session transcripts
- Circuit management — load and match circuit files by version, attribute count, and hash
- EC key extraction — extract P-256/P-384/P-521 public keys from X.509 issuer certificates
- ZK system spec negotiation — parse and match
ZkSystemSpecfrom DCQL JSON requests
| Requirement | Version |
|---|---|
| Swift | 6.0+ |
| iOS | 16.0+ |
Add the dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/eu-digital-identity-wallet/av-lib-ios-longfellow-zkp.git", from: "0.1.0"),
]
Then add LongfellowZkp to your target's dependencies:
.target(
name: "YourTarget",
dependencies: [
.product(name: "LongfellowZkp", package: "av-lib-ios-longfellow-zkp"),
]
),
The library is composed of two modules:
The main module containing the ZKP logic:
| Type | Description |
|---|---|
LongfellowZkSystem |
Core struct implementing ZkSystemProtocol. Manages circuits and performs proof generation/verification. |
LongfellowZkSystemSpec |
Describes a ZK specification: system name, circuit hash, version, number of attributes, and block encoding parameters. |
CircuitEntry |
Represents a loaded circuit file with its parsed spec and URL reference. |
LongfellowNatives |
Low-level bridge to the native C prover/verifier functions in MdocZK. |
NativeAttribute |
A namespace/key/value triple representing a single mdoc attribute for proof operations. |
Supporting utilities:
| Type | Description |
|---|---|
ECKeyExtractor |
Extracts EC public keys (P-256, P-384, P-521) from DER-encoded X.509 certificates. |
ProofVerificationFailureException |
Error thrown when proof verification fails. |
The pre-built native C library providing the Longfellow ZK prover, verifier, and circuit generation functions.
Circuit files follow the naming convention: <version>_<numAttributes>_<blockEncHash>_<blockEncSig>_<circuitHash>
Example filename: 6_1_4096_2945_137e5a75ce72735a37c8a72da1a8a0a5df8d13365c2ae3d2c2bd6a0e7197c7c6
From the app bundle:
import LongfellowZkp
// Automatically enumerate circuit files from the app bundle
let circuits = LongfellowZkSystem.enumerateLongfellowCircuits()
let zkSystem = LongfellowZkSystem(circuits: circuits)
From a specific URL:
let filename = "6_1_4096_2945_137e5a75ce72735a37c8a72da1a8a0a5df8d13365c2ae3d2c2bd6a0e7197c7c6"
let circuitURL = Bundle.main.url(forResource: filename, withExtension: nil)!
let circuit = try CircuitEntry(circuitFilename: filename, circuitUrl: circuitURL)
let zkSystem = LongfellowZkSystem(circuits: [circuit])
When a verifier sends a list of supported ZK system specs, find the best match:
// Parse specs from a DCQL JSON response
let jsonString = """
{
"zk_system_type": [
{
"system": "longfellow-libzk-v1",
"circuit_hash": "f88a39e561ec...",
"num_attributes": 1,
"version": 5,
"block_enc_hash": 4096,
"block_enc_sig": 2945
}
]
}
"""
let zkLongfellowSpecs = try LongfellowZkSystemSpec.parseFromJSONString(jsonString)
// Find a matching local circuit
let matchedSpec = zkSystem.getMatchingSystemSpec(
zkSystemSpecs: zkLongfellowSpecs.map(\.zkSystemSpec),
numAttributesRequested: 1
)
import SwiftCBOR
import MdocDataModel18013
// Build the ZkSystemSpec (typically received from a verifier's DCQL request)
let zkSpec = ZkSystemSpec(
id: "longfellow-libzk-v1_<circuitHash>",
system: "longfellow-libzk-v1",
params: longfellowSpec.toZkParams()
)
// Generate proof from a Document
let zkDocument = try zkSystem.generateProof(
zkSystemSpec: zkSpec,
document: document,
sessionTranscriptBytes: sessionTranscript,
timestamp: Date()
)
// Or generate proof from raw DeviceResponse bytes
let zkDocument = try zkSystem.generateProof(
zkSystemSpec: spec,
docBytes: deviceResponseBytes,
x: issuerPublicKeyX, // hex-encoded, e.g. "0x6789e9..."
y: issuerPublicKeyY, // hex-encoded
sessionTranscriptBytes: sessionTranscript,
timestamp: Date()
)
try zkSystem.verifyProof(
zkDocument: zkDocument,
zkSystemSpec: spec,
sessionTranscriptBytes: sessionTranscript
)
// If no error is thrown, verification succeeded.
Once configured, Eudi WalletKit will automatically generate zero-knowledge proofs for ZK-enabled requests in both proximity (ISO 18013-5) and OpenID4VP use cases. You only need the following source code to generate proofs, provided that you include the required circuits in the app bundle.
let circuits = LongfellowZkSystem.enumerateLongfellowCircuits(bundle: Bundle.main)
if !circuits.isEmpty {
wallet.zkSystemRepository = ZkSystemRepository(systems: [LongfellowZkSystem(circuits: circuits)])
}
You need also to include the circuits in your bundle. You can find them in the multipaz repository in the folder samples/testapp/src/commonMain/composeResources/files/longfellow-libzk-v1
let (x, y) = try LongfellowZkSystem.getPublicKeyFromIssuerCert(document: document)
// x and y are hex-encoded strings prefixed with "0x"
Circuit filenames encode their parameters:
<version>_<numAttributes>_<blockEncHash>_<blockEncSig>_<circuitHash>
| Component | Description |
|---|---|
version |
ZK specification version number |
numAttributes |
Number of attributes the circuit supports |
blockEncHash |
Block encoding parameter for hash |
blockEncSig |
Block encoding parameter for signature |
circuitHash |
SHA-256 hash identifying the circuit |
The MdocZK.xcframework bundles the native Longfellow static libraries for iOS device and simulator architectures. Use the provided script to rebuild it from pre-compiled static libraries.
The pre-compiled native static libraries can be obtained from the Multipaz repository at the folder multipaz-longfellow/src/iosMain/nativeLibs. Copy the architecture-specific folders into the same directory as the following create_xcframework.sh.
The script expects the following directory structure alongside create_xcframework.sh:
arm64-iphoneos/
lib/
libmdoc_static.a
libzstd.a
include/
mdoc_zk.h
arm64-iphonesimulator/
lib/
libmdoc_static.a
libzstd.a
include/
mdoc_zk.h
x86_64-iphonesimulator/
lib/
libmdoc_static.a
libzstd.a
include/
mdoc_zk.h
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_DIR="${SCRIPT_DIR}/output"
FRAMEWORK_NAME="MdocZK"
# Clean up previous builds
rm -rf "${OUTPUT_DIR}"
mkdir -p "${OUTPUT_DIR}/temp"
# Combine device libraries
echo "Combining device libraries..."
libtool -static \
"${SCRIPT_DIR}/arm64-iphoneos/lib/libmdoc_static.a" \
"${SCRIPT_DIR}/arm64-iphoneos/lib/libzstd.a" \
-o "${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_device.a"
# Combine simulator arm64 libraries
echo "Combining simulator arm64 libraries..."
libtool -static \
"${SCRIPT_DIR}/arm64-iphonesimulator/lib/libmdoc_static.a" \
"${SCRIPT_DIR}/arm64-iphonesimulator/lib/libzstd.a" \
-o "${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_sim_arm64.a"
# Combine simulator x86_64 libraries
echo "Combining simulator x86_64 libraries..."
libtool -static \
"${SCRIPT_DIR}/x86_64-iphonesimulator/lib/libmdoc_static.a" \
"${SCRIPT_DIR}/x86_64-iphonesimulator/lib/libzstd.a" \
-o "${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_sim_x86_64.a"
# Create fat binary for simulator
echo "Creating simulator fat binary..."
lipo -create \
"${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_sim_arm64.a" \
"${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_sim_x86_64.a" \
-output "${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_simulator.a"
# Create module maps
echo "Creating module maps..."
mkdir -p "${OUTPUT_DIR}/temp/device-headers/Modules"
mkdir -p "${OUTPUT_DIR}/temp/simulator-headers/Modules"
# Copy headers
cp "${SCRIPT_DIR}/arm64-iphoneos/include/"* "${OUTPUT_DIR}/temp/device-headers/"
cp "${SCRIPT_DIR}/arm64-iphonesimulator/include/"* "${OUTPUT_DIR}/temp/simulator-headers/"
# Create module map for device
cat > "${OUTPUT_DIR}/temp/device-headers/Modules/module.modulemap" <<EOF
module ${FRAMEWORK_NAME} {
header "../mdoc_zk.h"
export *
}
EOF
# Create module map for simulator
cat > "${OUTPUT_DIR}/temp/simulator-headers/Modules/module.modulemap" <<EOF
module ${FRAMEWORK_NAME} {
header "../mdoc_zk.h"
export *
}
EOF
# Create XCFramework
echo "Creating XCFramework..."
xcodebuild -create-xcframework \
-library "${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_device.a" \
-headers "${OUTPUT_DIR}/temp/device-headers" \
-library "${OUTPUT_DIR}/temp/lib${FRAMEWORK_NAME}_simulator.a" \
-headers "${OUTPUT_DIR}/temp/simulator-headers" \
-output "${OUTPUT_DIR}/${FRAMEWORK_NAME}.xcframework"
# Clean up temp files
rm -rf "${OUTPUT_DIR}/temp"
echo "✅ XCFramework created successfully at: ${OUTPUT_DIR}/${FRAMEWORK_NAME}.xcframework"
echo "✅ Done!"
Run it:
chmod +x create_xcframework.sh
./create_xcframework.sh
The output will be at output/MdocZK.xcframework.
| Package | Purpose |
|---|---|
| swift-certificates (X509) | X.509 certificate parsing for EC key extraction |
| SwiftCBOR | CBOR encoding/decoding for mdoc data |
| eudi-lib-ios-iso18013-data-model | ISO 18013-5 data model types (Document, DeviceResponse, IssuerSigned, ZkSystemSpec, etc.) |
swift test
Tests are located in Tests/av-lib-ios-longfellow-zkpTests/ and include:
- ZK system spec parsing from DCQL JSON
- Issuer public key extraction
- Full proof generation flow (prover + verifier)
- Document CBOR round-trip encoding
The released software is a initial development release version:
- The initial development release is an early endeavor reflecting the efforts of a short timeboxed period, and by no means can be considered as the final product.
- The initial development release may be changed substantially over time, might introduce new features but also may change or remove existing ones, potentially breaking compatibility with your existing code.
- The initial development release is limited in functional scope.
- The initial development release may contain errors or design flaws and other problems that could cause system or other failures and data loss.
- The initial development release has reduced security, privacy, availability, and reliability standards relative to future releases. This could make the software slower, less reliable, or more vulnerable to attacks than mature software.
- The initial development release is not yet comprehensively documented.
- Users of the software must perform sufficient engineering and additional testing in order to properly evaluate their application and determine whether any of the open-sourced components is suitable for use in that application.
- We strongly recommend to not put this version of the software into production use.
- Only the latest version of the software will be supported
Copyright (c) 2023 European Commission
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.