[SCITT] Re: Signed Statement identity when the ECDSA signature is not byte-unique

e.dogru@conarium.dev Fri, 21 August 2026 11:42 UTC

Return-Path: <e.dogru@conarium.dev>
X-Original-To: scitt@mail2.ietf.org
Delivered-To: scitt@mail2.ietf.org
Received: from localhost (localhost [127.0.0.1]) by mail2.ietf.org (Postfix) with ESMTP id F33EF12D4D64E for <scitt@mail2.ietf.org>; Fri, 21 Aug 2026 04:42:37 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ietf.org; s=ietf1; t=1787312557; bh=yAN9rXMO1VHVVt1gJQk/1DKFym/+8AdFdKUEmK2zrvc=; h=Cc:From:In-Reply-To:References:Subject:To:Date; b=gMMbZPlexrbwQTwdGqVz/OpXpnNUMpCHUTOLAbPksok7dE4n4TLpdoqP8BVoxiXN3 T8P0FUXXQtq44k6/+6UZrtt4bkBUE1VK31K7ZTqb3pnGCoU7d6lK+tOshggSUc7wK1 w3fc7iLrP7fyai8jOVoYiv78TzqfUW83hFPvgDmI=
X-Virus-Scanned: amavisd-new at ietf.org
X-Spam-Flag: NO
X-Spam-Score: -1.618
X-Spam-Level:
X-Spam-Status: No, score=-1.618 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_MESSAGE=0.001, HTML_MIME_NO_HTML_TAG=0.377, MIME_HTML_ONLY=0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H5=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001] autolearn=no autolearn_force=no
Authentication-Results: mail2.ietf.org (amavisd-new); dkim=pass (2048-bit key) header.d=conarium.dev
Received: from mail2.ietf.org ([166.84.6.31]) by localhost (mail2.ietf.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id pWfchXY4XDkq for <scitt@mail2.ietf.org>; Fri, 21 Aug 2026 04:42:37 -0700 (PDT)
Received: from bisque.yew.relay.mailchannels.net (bisque.yew.relay.mailchannels.net [23.83.220.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-256) server-digest SHA256) (No client certificate requested) by mail2.ietf.org (Postfix) with ESMTPS id 0A58512D4D64B for <scitt@ietf.org>; Fri, 21 Aug 2026 04:42:36 -0700 (PDT)
X-Sender-Id: hostingeremail|x-authuser|e.dogru@conarium.dev
Received: from relay.mailchannels.net (localhost [127.0.0.1]) by relay.mailchannels.net (Postfix) with ESMTP id F31D74C0795 for <scitt@ietf.org>; Fri, 21 Aug 2026 11:42:29 +0000 (UTC)
Received: from de-fra-smtpout4.hostinger.io (100-96-18-42.trex-nlb.outbound.svc.cluster.local [100.96.18.42]) (Authenticated sender: hostingeremail) by relay.mailchannels.net (Postfix) with ESMTPA id 403214C277A for <scitt@ietf.org>; Fri, 21 Aug 2026 11:42:25 +0000 (UTC)
X-Sender-Id: hostingeremail|x-authuser|e.dogru@conarium.dev
X-MC-Relay: Good
X-MailChannels-SenderId: hostingeremail|x-authuser|e.dogru@conarium.dev
X-MailChannels-Auth-Id: hostingeremail
X-White-Juvenile: 6d189c9658fc0963_1787312549936_1622030079
X-MC-Loop-Signature: 1787312549936:1399271124
X-MC-Ingress-Time: 1787312549936
Received: from de-fra-smtpout4.hostinger.io (de-fra-smtpout4.hostinger.io [148.222.55.14]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384) by 100.96.18.42 (trex/8.0.2); Fri, 21 Aug 2026 11:42:29 +0000
Received: from localhost (34.86.89.34.bc.googleusercontent.com [IPv6:2a00:1d35:17a4:d900:51f5:85e2:e74c:e841]) (Authenticated sender: e.dogru@conarium.dev) by smtp.hostinger.com (smtp.hostinger.com) with ESMTPSA id 4hRJLz49F0z44XM for <scitt@ietf.org>; Fri, 21 Aug 2026 11:42:19 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=conarium.dev; s=hostingermail-a; t=1787312539; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=38JKZJUtOM3H9ugP+w++wm3dEf4wJo/ONYUFOAJNW6c=; b=pM60OKUB2bhHRdrPCaSbkw0ajmSINnyccOdp67ccIRS8hf6EWpY90cTpmce8PzGNnFo4j6 TOJPT2Cin8D7/fKfv5YUS3RSPk78XGQ0TTJjO5UjMWo/6p8MXvHEm5sp5K2Qq2MkqnO4zD DjXRflPgPhIljkbfl9OeiPi4K4vKImwpQroWZPreZa/aK6V4LR54cTMH5YZkAnPO5WFLJA 45RrNbKH76v95zhl0uMSp/4F49z/MpJcJTvI2vk8zLdLKm3vo1wsubBVi9MjcXhgnTdcSa gBnruTNp3Lb9MxZIhIkA6a07fj4rNl+vMFJksa7fq8dx7fZ3+hAr/q0TtK0Eyg==
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset="utf-8"
From: e.dogru@conarium.dev
In-Reply-To: <BYAPR19MB280617ED814037356517C204ADA42@BYAPR19MB2806.namprd19.prod.outlook.com>
Message-Id: <1787312526938859340.1787312526@conarium.dev>
Mime-Version: 1.0
References: <CAMkGB8MLjYS8vohFpzcuA=hDiQKz3MMnndaq-H46jLM7aVbGvQ@mail.gmail.com> <BYAPR19MB2806870CAB4F65EA5BCDF3DFADA52@BYAPR19MB2806.namprd19.prod.outlook.com> <mEo2fD30wxCFTsWzSO-0JGalbUi1yRMxkVcRnhwOlhlgZoQ_EfIQx84PyyufVwGDZDCnDmY2-OOREsjEOj7-Tdp-vqINESe6UzjmpXu1vlM=@vaara.io> <BYAPR19MB280617ED814037356517C204ADA42@BYAPR19MB2806.namprd19.prod.outlook.com>
To: jhillier=40certisyn.com@dmarc.ietf.org
Date: Fri, 21 Aug 2026 11:42:19 +0000
X-CM-Analysis: v=2.4 cv=ALriHGRn c=1 sm=1 tr=0 ts=6a88399b a=RddCdUNZqxBAE8jYSUBS9Q==:617 a=xqWC_Br6kY4A:10 a=IkcTkHD0fZMA:10 a=48vgC7mUAAAA:8 a=vsPweqNngbByKnjsFioA:9 a=gwpxBBvPGrsE4pwA:21 a=frz4AuCg-hUA:10 a=QEXdDO2ut3YA:10
X-CM-Envelope: MS4xfPzFT5OKD5yHEswsEAb7MWeI/gcUtixeJxHk64GDqdF6rHC682TePREmJqdsrTW9WTlgfjbIiXF6ncRmB6wv5aKeTql4BB0W75X2qanWab0jGyNlMtUs 0nmj3aBdJgZrQN+8Tap0pdunrQ1ljtENC8v6nDM+cBES7WGfiE0+7XBHr0MtkGhZAEqfFnybi2eAJUmZ6DMU5gxc3SwzHJ0WqB8VcB7K2QwAhoLzSpQyHhbc aMeFpkuevn8rIiaE5EmzBg==
X-AuthUser: e.dogru@conarium.dev
Message-ID-Hash: P4PR46UDYERNGTY32IBGRRLDHXHJ3CUC
X-Message-ID-Hash: P4PR46UDYERNGTY32IBGRRLDHXHJ3CUC
X-MailFrom: e.dogru@conarium.dev
X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header
CC: scitt@ietf.org, hello@vaara.io, anton.sokolov@tyche.institute
X-Mailman-Version: 3.3.9rc6
Precedence: list
Subject: [SCITT] Re: Signed Statement identity when the ECDSA signature is not byte-unique
List-Id: "Supply Chain Integrity, Transparency, and Trust" <scitt.ietf.org>
Archived-At: <https://mailarchive.ietf.org/arch/msg/scitt/tJdCBXZdkJZt7mRC6tJneJtGW5Y>
List-Archive: <https://mailarchive.ietf.org/arch/browse/scitt>
List-Help: <mailto:scitt-request@ietf.org?subject=help>
List-Owner: <mailto:scitt-owner@ietf.org>
List-Post: <mailto:scitt@ietf.org>
List-Subscribe: <mailto:scitt-join@ietf.org>
List-Unsubscribe: <mailto:scitt-leave@ietf.org>

Anton, Henri, Joel,

Two things in this exchange are worth more than the ECDSA case, and I want to
report against both rather than agree with them, because we failed one of them
today.

**The repair is a rule over the class, not a sixth item.** Joel's Override Record
failed no test that was applied to it, because no test was applied to it, and the
fix was to state the property over every preimage the document defines rather than
to extend the list. Keeping the enumeration as a reader's aid *with a note saying
that is all it is* — that is the part I would copy verbatim.

We reached the same shape from a different direction and it is worth one data
point. A guard here checks that every conformance label still has a case
producing it, so that deleting the last case for a label cannot silently shrink
the class while the suite stays green. The label list is not written in the
guard; it is read from the tool's own output, because a written list is correct
until someone adds a field, which is your sentence about correctness that expires
on the next revision.

**And Anton's sentence is the one I would put on a wall.** *A checker that
rebuilds the preimage it was meant to perturb reports exactly the property it was
written to demonstrate, and it passes.*

I have a fresh instance of it, from today, in a guard I wrote to pin the order in
which our verifier runs its checks. The order matters because putting the
signature check first makes a deleted record report as a bad signature — true
verdict, wrong reason. So the guard builds records that fail two checks at once
and asserts which failure is named.

Its first version removed the signature from *every* record in the file. That
makes the file's first record fail the signature check before the chain break is
ever reached, so the guard reported "signature" for a verifier whose order was
correct. It was measuring its own fixture. It now learns which record the
pristine vector already fails on, from the tool's own JSON output, and unsigns
exactly that one — plus a control asserting that a record whose only defect is a
missing signature does fail on the signature. I would not have caught it if it
had gone green the first time.

**On the format itself, one structural note, useful only as an existence proof.**

Our identity is not the served envelope. The content hash is computed with the
signature, the hash field itself, and the anchor excluded from the preimage, and
the signature is then taken over that hash. So Joel's rule — every signature made
by a party other than the one computing the digest is replaced by its signing
input digest — holds here vacuously, because no signature enters any preimage at
all. Two encodings of one signature are one identity by construction.

We did not choose that to defeat malleability. We chose it so a transparency-log
anchor could be attached after signing without invalidating the record, which is
mild evidence that the repair is structural rather than a patch for ECDSA.

And the discrimination we lost, since Joel is right that writing that down is the
other half of the test: excluding the signature from the preimage means an
operator who controls the file can strip or thin the signature without
invalidating the content hash. We document it as not solvable in-file. What runs
against it is a keyed HMAC alongside Ed25519 — strip the signature, recompute the
unkeyed hashes, and the HMAC still fails, which a test covers — plus the external
anchor.

Emek


On Fri, Aug 21, 2026 at 12:06 AM Joel Hillier <jhillier=40certisyn.com@dmarc.ietf.org> wrote:

Hi Anton, Henri,

The files are run. Result first, then a correction to something I told this list on Wednesday.

Both digests match what you published before I opened them. Both 395 bytes, protected header and unprotected header and payload byte-identical, only the 64-octet signature differing. r equal, s2 exactly n - s1, both verifying against the attestation-certificate key. One Sig_structure, so one Signing Input Digest, e104e763c87f8bda.... Your protected header decodes to {1: -7, 4: "piv-9c/37587575"}, and slot 9c is the digital-signature slot, which matches your account of the device.

Against ARP's ledger constructions: -03 Prior-Entry Hash differs, -04 is the same. Merkle leaf over the served envelope gives two leaves for one act, over the Sig_structure gives one. So the repair does what it was written to do, on bytes a secure element produced ratherthan ones I generated to prove a point.

There's one thing in your files you didn't mention, and I think it makes the case worse rather than better. Your original is high-S and the twin is low-S. The twin isn't an exotic artefact. It's the canonical low-S normalisation of the original, which is exactly what an implementation following the usual hardening advice produces on receipt.

So the second identifier doesn't need an attacker at all. A well-behaved middlebox that normalises s on ingest manufactures it, out of the honest signer's own bytes, while believing it's improving matters. "Mandate low-S" doesn't merely fail to close this. Applied to an artefact already in flight, it's one of the ways the gap gets opened. Wortha line in the payload-binding draft, because low-S is the first thing everyone reaches for and it's being reached for in the wrong place.

Now the correction. I said five constructions. It's six.

I ran a fresh adversarial pass over the whole document yesterday, on Henri's depth rule rather than on the thing I was looking at, and it found a sixth carrier that both of my earlier sweepswalked past.

ARP has an Override Record: where an adversarial pre-transmission test fails and a human operator authorises the reconciliation anyway, the Output carries a record of that, and the lastelement of the record is a COSE_Sign1 by the authorising operator. Deliberately by the operator and not by the server, because a record the server signs attests only that the server says someone authorised it. That signature is a field of the ReconciliationOutput. The Reconciliation Hash preimage names two carriers to be replaced by Signing Input Digests, the register signature in each Query Binding Record and the register signature in each Non-Answer Statement, and the Override Record is neither.

So a third party holding an override signature substitutes (r, n-s), and the Reconciliation Hash moves. It's the ledger index, so the entry is indexed under a value the requester can't reproduce. It's a field of every ledger entry, so the Self-Entry Hash moves, so the Entry Signature payload moves, sothe next entry's Prior-Entry Hash doesn't match. A reader concludes the chain is broken when the operator equivocated about nothing, which is the sentence I wrote in the security section for a case one level shallower than this one.

How it was missed is the part worth having. The first sweep looked for digests taken over signed artefacts, found the Reconciliation Hash embedded a register signature, and repairedthat. The Override Record is a signature by a party who is neither the server nor a register, travelling to the server over a channel the server doesn't control, sitting in a field the preimage already carried. It failed no test that was applied to it. Notest was applied to it.

Henri, this is your breadth rule with the last step taken. One file being right is no evidence about the next one, and an enumeration of carriers is right until someone adds a field. Sothe repair isn't a sixth item on the list. The document now states the rule over the class: in any preimage this document defines, every signature made by a party other than the party computing the digest is replaced by its Signing Input Digest. The enumeration stays as a reader's aid, with a note saying that is all it is. Correctness thatexpires on the next revision isn't the property the section exists to establish.

I'd suggest the payload-binding draft state it that way too, for the same reason. "Key the identifier on the signed payload" is right and is about one artefact. "No preimage carries a signaturemade by a party other than the one computing the digest" is the rule that survives someone adding a field to that artefact.

The wrong answer, since I said I'd report those too.

Take a holder of the original who's shown an inclusion proof issued over the twin. Under -03, ARP's leaf-binding rule requires the verifier to recompute the leaf from the object and refuse the proof when it doesn't match. It refuses. That's correct, and it's the right outcome.

What the document didn't give it was any way to say why. The section said MUST refuse and stopped. ARP had reason codes for a stale attestation, an unverifiable attestation, an unresponsiveregister, an exhausted budget and an incomplete notarisation, and nothing at all for "the object I hold and the object this proof commits to are the same signing act under two encodings."

Henri, that's your false split arriving in the vocabulary rather than in the digests. The digest is stable now and the report was still wrong, because a deployment reporting this as detectedtampering reports something that didn't happen, and it'll be believed, since the check that produced it worked exactly as specified. Fixing the construction and leaving the error taxonomy alone gets you a correct refusal with a false explanation attached.

There's now a verification-outcome vocabulary with four values, and one rule that matters: where the artefact a verifier holds and the artefact the proof commits to have equal Signing InputDigests and unequal enveloped bytes, it MUST report that and MUST NOT report a leaf mismatch or a root mismatch. Same-act-two-encodings is evidence of a re-encoding and of nothing else, and it has to be tested for before either of the outcomes that accusesomebody.

The same pass found one reason code carrying five distinct causes, because four other sections pointed at it for conditions its own definition never named: a failed signature, an echo mismatch,a query-binding mismatch, a fabricated source-data identifier, and a register attesting something the registry says it can't. A register operator reading its own entry was told something was wrong with its attestation and not whether the fault was its own.Now five values.

And a residual that isn't in ARP's digests any more.

Evaluation Sweep Statements chain by carrying the transparency-service identifier and EntryID of the previous Statement's notarisation. If a Transparency Service keys EntryID on the envelopedbytes, one Statement has two EntryIDs available to it, and a party walking the chain backwards from a pointer to one of them holds a pointer the other copy's chain doesn't reach. SCRAPI retrieves by EntryID and offers no query surface, so that party can'tsearch for the other one.

That series is ARP's evidence that a sweep it was obliged to perform was performed. A chain a third party holding no key can make look broken, using bytes the honest signer produced, isa weaker instrument than my own section claims. It isn't ARP's defect and ARP can't repair it alone, but ARP can state what it assumes about EntryID stability, and now does. That's a concrete consequence for the SCRAPI point rather than a hypothetical one.

The rest, briefly.

Henri, your restatement of the rule is the right one and it isn't what I said. "A digest can carry a signature it never names, through an object someone else signed" is the finding. YourTuesday sentence was breadth. The Reconciliation Hash was depth: a construction that names an exclusion, honours it exactly, and is still over signature-bearing bytes because the preimage embeds an artefact a third party signed. The Override Record is bothat once, which is presumably why it survived two sweeps.

On your third finding, the direction distinction is the part I'd carry. Tuesday's two let two byte-different artefacts claim one signing act. grant_fingerprint lets one act produce two identities. A false merge collapses a distinction that's real and an event gets under-counted. A false split manufactures one that isn't, and a verifier recomputing from the grant it holds fails to matcha receipt for an authorization that did govern. A false merge loses information. A false split manufactures an accusation.

A sweep written for one direction won't find the other, which is why you hadn't looked. One asks whether two artefacts can reach one identifier that should have been two; the other askswhether one act can reach two identifiers that should have been one. Different queries over the same tree, and the first returns nothing at a site that only fails the second way.

And the test you applied to your own repair is the one I'd name explicitly. You checked that the intent survives: over the signed payload and the signing key, a re-mint under a differentkey or a changed scope still moves the value, and a malleated twin doesn't. A repair has to preserve every discrimination the construction was written to make, and drop only the ones that were never real. Mine failed that in one place and only stating thefailure made it visible: the rule that an entry is signed exactly once had a verifier-side test before the change and doesn't have one after. Checking the discriminations you keep is half the test. The other half is writing down the one you lost.

Anton, on the credit: yes, happy to be named, and I'd rather the draft carry the rule than the names. "A checker that rebuilds the preimage it was meant to perturb reports exactly the propertyit was written to demonstrate, and it passes." That's yours as a sentence, it's sharper than mine, and it generalises past signatures to any harness that derives its expected value from the subject under test.

And your correction lands on the same form mine did, independently. The supportable claim is that RFC 9053 2.1 constrains neither r nor s, and that SEC1 4.1.4 and FIPS 186-5 6.4.2 range-check only. Two people retracting the same locator in the same week is a fair argument that 4.1.3 is a citation the whole area has been passing along without opening.

Transcript and the script are in the repo under _audit/anton/, no arguments, standard library plus cryptography.

Joel