# Interoperating Is Not the Same as Being Right

Interop events tend to look alike. Implementations built by different organizations get paired off, presentations go back and forth, verification succeeds, and a green mark goes into the cell. The more of the grid turns green, the better the room feels.

It's worth asking what that green actually proves.

Two implementations working together does not mean both are correct. They may be wrong in the same way. What an interop test measures is not conformance to the specification but **agreement between two interpretations**. If the interpretations line up, the pair passes, including when both have drifted from the spec in the same direction.

And that case is more dangerous than an outright failure, because it strengthens conviction. If your implementation talks cleanly to someone else's, it feels like proof that you weren't just imagining things. It feels like validation.

## The things interop can never catch

There's a deeper problem. A significant portion of what a specification requires cannot be tested by interoperability at all.

Take the key used to encrypt the response. Under HAIP's `direct_post.jwt`, the verifier includes its public key in the request, the wallet encrypts the response to that key, and the verifier decrypts with the corresponding private key. That key pair is supposed to be generated fresh for every request.

Suppose a verifier generates one key pair and reuses it indefinitely.

Nothing happens. Encryption works, decryption works, the response is processed without incident. And more importantly: **there is no way for the wallet to find out.** All the wallet receives is a public key. Whether that key was generated a second ago or has been in use for six months, nothing on the receiving side distinguishes the two.

But the spec asks for a fresh key for a reason. Reuse one and a single compromise of that key opens every response ever encrypted to it. Capture traffic over a long period, obtain the private key later, and the whole archive decrypts. Generate a new key per request and a compromise is contained to that one exchange. That property, one breach not becoming all of them, is forward secrecy.

So this isn't wrong because it fails to work. It's wrong in a way you can't notice **precisely because it works**. And this case goes one step further: the fact isn't present in anything the other implementation can observe. Pair two implementations and run as many cases as you like: this requirement sits outside the observable surface. Nothing short of testing the implementation against the specification will reach it.

Requirements of this kind are scattered throughout the specification. There is a region that the interoperability lens, by construction, cannot see.

## A verifier exists in order to reject

Second: interop events are largely about aligning the success path. Present a valid credential, confirm it verifies.

But think about why a verifier exists and that's only half the job. A verifier's work isn't acceptance. It's discrimination. Letting valid presentations through is the *minimum* condition; the substance of the role is refusing the ones that aren't valid.

Put bluntly, a verifier that accepts absolutely anything scores full marks at an interop event. It pairs successfully with everyone.

Whether it correctly rejects an invalid signature. An expired credential. An audience that isn't it. A wrong nonce. These only surface in negative tests, and they are very nearly the whole of what a verifier does.

That's why a conformance suite carries positive and negative tests together. Checking only the success path verifies half a verifier.

## What conformance testing actually fills in

The value of conformance testing isn't convenience or speed. It's that it **changes what you are compared against**.

In interop, you compare your implementation to someone else's. In conformance, you compare it to the specification. The first asks whether there is agreement. The second asks whether it is correct. These are different questions, and an answer to the first is not an answer to the second.

This was the gap we kept running into while implementing these specs at Hopae: we needed a way to validate against the specification itself rather than against each other's readings of it. That's what makes interoperability real rather than aspirational.

We took part in the OpenID Foundation's interop events as implementers, ran the suites from early on, and fed what we found back to the DCP Working Group. Being in that process meant seeing up close what the suites catch, and how different that is from what shows up at an interop event.

## August 8

On 7 August 2026 the OpenID Foundation completed the HAIP 1.0 conformance suites for OpenID4VP and OpenID4VCI and opened them for self-certification.

The following day, 8 August, Hopae self-certified against two profiles: OpenID4VP 1.0 + HAIP 1.0 as a Verifier (sd\_jwt\_vc, direct\_post.jwt), and OpenID4VCI 1.0 + HAIP 1.0 as an Issuer (sd\_jwt\_vc, issuer\_initiated).

*   [Certified OpenID4VP 1.0 + HAIP 1.0](https://openid.net/certification/certified-oid4vp-haip-final/)
    
*   [Certified OpenID4VCI 1.0 + HAIP 1.0](https://openid.net/certification/certified-oid4vci-haip-final/)
    

The date isn't the point. What it reflects is that we had been waiting for this reference to exist, and that we knew why we wanted it before it arrived.

## Agreement and reference

Interoperability is an outcome. Conformance is the condition under which that outcome holds.

A set of implementations that all work together is a byproduct of each one being aligned to the same reference. Interoperability reached the other way, by aligning to each other, breaks the moment the participant count grows. Reconciling two interpretations takes one negotiation. Reconciling ten takes forty-five. And none of those negotiations tells you what the specification actually requires.

Align to the reference instead and it takes ten. More to the point, when the eleventh implementation arrives, nothing has to be redone.
