When verifying a Verifiable Credential using the Ed25519Signature2018 suite, the ssi library rejects valid signatures generated by standard compliant third-party issuers (such as Digital Bazaar and Danubetech) with a ProofValidationError::InvalidSignature.
The root cause is a serialization mismatch during the JWS Detached verification process. ssi-jws decodes the header into a Rust struct and then re-serializes it via encode_signing_bytes(). Because serde_json serializes the struct fields in declaration order (e.g., alg, crit, b64), the resulting base64url string differs from the original issuer's string if they ordered the JSON properties differently (e.g., alg, b64, crit as generated by Digital Bazaar). This alters the signing input bytes and completely breaks the mathematical validation of the Ed25519 signature.
To Reproduce
Attempt to verify the following standard AlumniCredential generated using the Digital Bazaar implementation.
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
{
"alumniOf": "https://schema.org/alumniOf",
"AlumniCredential": "https://example.org/AlumniCredential"
}
],
"id": "urn:uuid:1234",
"issuanceDate": "2026-05-20T22:40:24Z",
"type": [
"VerifiableCredential",
"AlumniCredential"
],
"issuer": "did:key:z6Mkh47kdEbVR8Q15zwDcFGNQ7Xd8W3r56MKNN6ouZLrKBoQ",
"credentialSubject": {
"id": "did:example:holder",
"alumniOf": "Example University"
},
"proof": {
"type": "Ed25519Signature2018",
"created": "2026-05-20T22:42:45Z",
"verificationMethod": "did:key:z6Mkh47kdEbVR8Q15zwDcFGNQ7Xd8W3r56MKNN6ouZLrKBoQ#z6Mkh47kdEbVR8Q15zwDcFGNQ7Xd8W3r56MKNN6ouZLrKBoQ",
"proofPurpose": "assertionMethod",
"jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..FQ9H02q8qI3gP_ScK9bNV62FrCZOAqbRKxQXEee4eq0FbBSobGSUx58vw6rscy5e2UJIDLU6vofkBagqBTJ5BA"
}
}
-
Result: ProofValidationError::InvalidSignature
-
Reason: The raw JWS header is eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19 ({"alg":"EdDSA","b64":false,"crit":["b64"]}). ssi re-serializes this to eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9 ({"alg":"EdDSA","crit":["b64"],"b64":false}), corrupting the signing input.
Proof of Concept / Workaround
To demonstrate the root cause, I applied a temporary workaround in ssi-data-integrity/core/src/signing/jws.rs (inside the verify function for DetachedJwsSigning). By bypassing the encode_signing_bytes() call and extracting the raw header directly from the JWS string, the signature validates successfully:
// Workaround in `verify`:
let raw_jws = proof.signature.jws.as_str();
let raw_header_b64 = raw_jws.split('.').next().unwrap_or("");
let mut signing_bytes = raw_header_b64.as_bytes().to_vec();
signing_bytes.push(b'.');
signing_bytes.extend_from_slice(prepared_claims.as_ref());
// Proceed to verify...
When verifying a Verifiable Credential using the Ed25519Signature2018 suite, the ssi library rejects valid signatures generated by standard compliant third-party issuers (such as Digital Bazaar and Danubetech) with a ProofValidationError::InvalidSignature.
The root cause is a serialization mismatch during the JWS Detached verification process. ssi-jws decodes the header into a Rust struct and then re-serializes it via encode_signing_bytes(). Because serde_json serializes the struct fields in declaration order (e.g., alg, crit, b64), the resulting base64url string differs from the original issuer's string if they ordered the JSON properties differently (e.g., alg, b64, crit as generated by Digital Bazaar). This alters the signing input bytes and completely breaks the mathematical validation of the Ed25519 signature.
To Reproduce
Attempt to verify the following standard AlumniCredential generated using the Digital Bazaar implementation.
{ "@context": [ "https://www.w3.org/2018/credentials/v1", { "alumniOf": "https://schema.org/alumniOf", "AlumniCredential": "https://example.org/AlumniCredential" } ], "id": "urn:uuid:1234", "issuanceDate": "2026-05-20T22:40:24Z", "type": [ "VerifiableCredential", "AlumniCredential" ], "issuer": "did:key:z6Mkh47kdEbVR8Q15zwDcFGNQ7Xd8W3r56MKNN6ouZLrKBoQ", "credentialSubject": { "id": "did:example:holder", "alumniOf": "Example University" }, "proof": { "type": "Ed25519Signature2018", "created": "2026-05-20T22:42:45Z", "verificationMethod": "did:key:z6Mkh47kdEbVR8Q15zwDcFGNQ7Xd8W3r56MKNN6ouZLrKBoQ#z6Mkh47kdEbVR8Q15zwDcFGNQ7Xd8W3r56MKNN6ouZLrKBoQ", "proofPurpose": "assertionMethod", "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..FQ9H02q8qI3gP_ScK9bNV62FrCZOAqbRKxQXEee4eq0FbBSobGSUx58vw6rscy5e2UJIDLU6vofkBagqBTJ5BA" } }Result: ProofValidationError::InvalidSignature
Reason: The raw JWS header is eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19 ({"alg":"EdDSA","b64":false,"crit":["b64"]}). ssi re-serializes this to eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9 ({"alg":"EdDSA","crit":["b64"],"b64":false}), corrupting the signing input.
Proof of Concept / Workaround
To demonstrate the root cause, I applied a temporary workaround in ssi-data-integrity/core/src/signing/jws.rs (inside the verify function for DetachedJwsSigning). By bypassing the encode_signing_bytes() call and extracting the raw header directly from the JWS string, the signature validates successfully: