[SCITT] First-failure list: draft-dogru-cedulon-04

Tiago Pinto <tiago@donttrustverify.pt> Sun, 30 August 2026 15:23 UTC

Return-Path: <tiago@donttrustverify.pt>
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 2F63C131D5214 for <scitt@mail2.ietf.org>; Sun, 30 Aug 2026 08:23:46 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ietf.org; s=ietf1; t=1788103426; bh=phpzFPYgibL/9iqcW08A/39D+L89gsYZPHR+xjnnovk=; h=Date:To:From:Cc:Subject; b=yTXLqGSqzYOOO4RoY6D/7exVPozylq5NZ0m2XZ3iyzE+dFMvRYqSUxcW4mW18kBO2 TwTHZbcpoKrZJhCgk6DIfTXkkxezZIMCJRGqqyGRUDP3ZMnZ4q8uPv8sNrHb3tslmc Ev6AKRW7jNf4wJ4h3bQgMqQ1yrT5nOnJrDHflHCI=
X-Virus-Scanned: amavisd-new at ietf.org
X-Spam-Flag: NO
X-Spam-Score: -2.797
X-Spam-Level:
X-Spam-Status: No, score=-2.797 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, RCVD_IN_DNSWL_LOW=-0.7, 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_PASS=-0.001] autolearn=ham autolearn_force=no
Authentication-Results: mail2.ietf.org (amavisd-new); dkim=pass (2048-bit key) header.d=donttrustverify.pt
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 1SVBLp0EgXyE for <scitt@mail2.ietf.org>; Sun, 30 Aug 2026 08:23:43 -0700 (PDT)
Received: from mail-4317.protonmail.ch (mail-4317.protonmail.ch [185.70.43.17]) (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 D0CDA131D520F for <scitt@ietf.org>; Sun, 30 Aug 2026 08:23:42 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=donttrustverify.pt; s=protonmail; t=1788103421; x=1788362621; bh=lsxCdiVk2vQDHW3haf1IZ5ewxRQBzfr+0cfCABi6XRs=; h=Date:To:From:Cc:Subject:Message-ID:Feedback-ID:From:To:Cc:Date: Subject:Reply-To:Feedback-ID:Message-ID:BIMI-Selector; b=x/FWjarqUgGnjvdTTimUNd/dn+6jW2jFhDreFkF5G8kSLtBEm0UESAZyZFCZGVUQ+ HYgiixukoLkiGPYM5jo1pLtM2aKerfdFRBn0pXI7KQ1wnaWSDINizpF0BW690fvofC D5xZYotLhts1qrxmVpxDEYziLg05PZbS9nBmqfRuVWzaXbOzsXRAGE9xHqypfNWdQp a+iEIvksGFAqrHafVXNujJU601hvTJg+eoTherFhd0S/7ebqUXNYqklgtqq7RlcFsn cHg0MWX/DXQPLfEUeF1fCM9zq8wbHJAoe4boZb7dlPRrgz1cqidaRq1u8tBeuP+AeK 8+pXXT1Rvhoqg==
Date: Sun, 30 Aug 2026 15:23:36 +0000
To: "scitt@ietf.org" <scitt@ietf.org>
From: Tiago Pinto <tiago@donttrustverify.pt>
Message-ID: <465ITu9UnGDKZqwWdqZD94K1LFpJxYWWBF-ztLXONI909Mk8_ISvfB71dAY_PDolw0SE14uJyBQeRafcV4gBQqFjfYLf7xVrjuIewnurNJQ=@donttrustverify.pt>
Feedback-ID: 206570780:user:proton
X-Pm-Message-ID: 0d8659603f671ba903198b105b684fc249fc44b9
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Message-ID-Hash: O4QSUUTN7ZDP3SUX5M42EBZJ33VEGCXC
X-Message-ID-Hash: O4QSUUTN7ZDP3SUX5M42EBZJ33VEGCXC
X-MailFrom: tiago@donttrustverify.pt
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: "e.dogru@conarium.dev" <e.dogru@conarium.dev>
X-Mailman-Version: 3.3.9rc6
Precedence: list
Subject: [SCITT] First-failure list: draft-dogru-cedulon-04
List-Id: "Supply Chain Integrity, Transparency, and Trust" <scitt.ietf.org>
Archived-At: <https://mailarchive.ietf.org/arch/msg/scitt/7DOQshWFo96zAGxizd8h23AELoI>
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>

Hi Emek, all,

Taking up your offer from the thread on
draft-pinto-agent-authz-contestability. I put -04 through the same kind
of reading.

I ran the Appendix A vectors before reading for failure points. Both
Ed25519 signatures verify, the SPKI-derived kid is
06e3fd8fda29bb60, and deterministic re-encoding reproduces both
protected headers and payloads byte for byte. Run against the exact
IETF archive bytes, SHA-256
661755c600aede25451ce3a67df4a45d0d964c7b9196dc725dd310723eb8a49f,
under Python 3.14.4, cbor2 6.1.4, and cryptography 50.0.1.

My first failure is the transparency-receipt path.

1. I cannot implement steps 15 and 16 from the evidence the draft
   defines.

Section 11.3 says that "A receipt binds a statement hash, not a
statement" and that "the verifier computes the statement hash of each
presented checkpoint itself."

I do not think that is the verification mechanism defined by RFC 9942.

RFC 9942 Section 5.2.1 requires the verifier first to apply the
inclusion proof to the bytes of the candidate entry. The resulting
Merkle Tree root becomes the detached COSE_Sign1 payload, and the
signature is then verified over that root.

For the SCITT use here, the candidate entry is the registered Signed
Statement, not merely the checkpoint COSE object carried inside it. The
audit evidence defined by -04 gives me the checkpoint, but I cannot find
a requirement to present the exact outer Signed Statement bytes that
were registered.

That leaves two separate failures for an implementation. I do not have
the candidate-entry bytes required to apply the RFC 9942 inclusion
proof. Even if I had them, step 15 tells me to verify the receipt
against the transparency key while the surrounding text describes this
as authenticating a statement hash. RFC 9942 instead requires candidate
entry, inclusion proof, Merkle root as the detached payload, and then
signature verification.

Section 18 already says that the current implementation treats a
receipt as a signature over a statement rather than as proof of log
membership, and that it has not been exercised against a real
Transparency Service. My point here is narrower: the same simplification
appears to have reached the protocol text and verification algorithm.

Therefore I cannot implement the RFC 9942 verification path from the
inputs and algorithm as currently stated.

2. I cannot determine the verification key or the resulting finding
   consistently in the unpinned branch.

The Abstract says:

"No signed object may be verified against a key it carries itself"

MUST-T4-9 likewise requires the issuer public key out of band.

The finding table nevertheless describes the unpinned case as one in
which the receipt or checkpoint was checked using a key associated with
the presented object. The COSE profile itself carries kid, but no public
key. The unprotected header MUST be empty, and I cannot find a COSE_Key,
x5chain, or other field from which to obtain the candidate public key.

So in the no-pin case I cannot determine what key the signature is to
be checked against.

Step 4 then adds a second ambiguity. It first says to reject on Ed25519
failure or kid mismatch, and then says to apply the issuer root and
report issuer-key-mismatch.

For example, if kid differs from the configured issuer key, I cannot
tell whether that is a rejection only or an issuer-key-mismatch
finding. If kid matches but the signature is invalid under that key, I
cannot tell whether the resulting condition is issuer-key-mismatch,
receipt-chain-break, or something else.

Those choices matter because a rejected receipt is removed from the
attested set before reconciliation.

Therefore I cannot derive one key-resolution path and one finding set
from step 4.

3. I cannot construct one authenticated Rail Extract from the
   specification.

Table 8 says:

"Each settlement record MUST contain"

ref, amount, currency, and timestampMs.

The Authentication text then says:

"The member names of that body are the rail's to define"

Those are different rules. If the Table 8 names are normative, a rail
cannot rename them. If the rail defines the names, Table 8 has not
defined the signed record shape.

I also cannot derive one complete signed JSON object containing the
record set, windowStartMs, windowEndMs, the account and rail identifiers
required by the scope rules, and the signature representation. Table 8
additionally uses tstr and uint, which are CBOR terminology, for a body
that the next section says is JSON.

Since the authenticated extract is the authoritative reconciliation
input, therefore I cannot construct or verify the same Rail Extract
from the text alone.

4. I cannot reconstruct one receipt order or one checkpoint-window
   result.

Step 6 says:

"Walk the attested receipts in issuer order."

I cannot find a definition of issuer order.

Presentation order, timestampMs order, and hash-chain order can differ.
The draft itself notes that timestampMs is issuer-asserted, so temporal
order cannot safely be assumed to be chain order.

The same problem appears in chainHeadHash, which is the receipt hash of
the "last receipt in that window". I cannot determine what makes a
receipt last.

There are two further boundary cases in step 12. Every chained receipt
MUST fall in exactly one checkpoint window, but the document also
contemplates audits with no checkpoints. With receipts and zero
checkpoints, I cannot determine whether every receipt produces
window-coverage or whether that check is skipped. The same question
occurs for receipts in the currently open epoch after the last closed
checkpoint.

The dependency text also says "Nothing else in this list feeds another
step." Taken literally, that is not true. Step 14 consumes checkpoints
accepted by step 11, and steps 8 and 9 consume the index built by step
7. That matters because the same section permits evaluation in any
order only where the resulting finding set is unchanged.

Therefore I cannot derive one ordering rule, one definition of the
in-window chain head, or one result for zero-checkpoint and open-epoch
evidence.

5. Independent issuer and rail clocks can produce a completeness
   failure for an honest payment.

Step 5 scopes receipts using the issuer's timestampMs. The extract is
scoped using the rail's timestampMs.

Consider a legitimate payment close to windowEndMs. If the rail records
it just before the boundary and the issuer records its receipt just
after, the settlement is inside the extract while its matching receipt
is outside the reconciliation set. The reverse can happen with the
clocks skewed the other way.

Step 8 then reports settlement-without-receipt or
receipt-without-settlement.

This is not a same-evidence interoperability difference. Both
verifiers can reach exactly the same false failure.

Therefore I cannot see how a deployment with independent honest clocks
avoids audit failures at window boundaries. I think one timestamp needs
to control reconciliation membership, or the boundary treatment needs
to be defined.

6. I can make an otherwise valid audit fail by appending a
   countersignature.

Section 10.2 says:

"Anyone holding an honest receipt can append a countersignature of their
own"

That is the right observation.

But if the verifier has a payee key pinned, a bad countersignature or
one under the wrong key causes the audit to fail.

Take an honest issuer receipt that otherwise passes the audit. A party
presenting the evidence appends an arbitrary countersignature. The
issuer signature and receipt bytes have not changed, but the audit can
now fail.

That seems to give an appendable object that is not covered by the
issuer signature the ability to manufacture a negative result. It is
also close to the attack that the revised MUST-T8-9 avoids by refusing
to let unauthenticated evidence create an accusation.

Therefore I cannot see why a countersignature that cannot be attributed
to the pinned payee should fail the underlying audit rather than be
excluded with a finding or warning.

7. I cannot close the counterparty binding across manifest, receipt,
   and rail extract.

The Trade Manifest does not identify the payee. The receipt does.

The manifest comparison checks amount, currency, and expiry. The rail
reconciliation checks ref, amount, and currency. The Rail Extract
record defined in Table 8 does not contain a payee.

Consider a valid manifest M published by A for 100 USD, a receipt
naming M but paying B, and an extract row matching the receipt's ref,
amount, and currency. The comparisons defined by the algorithm can all
succeed.

Perhaps a given rail guarantees that ref independently resolves to the
beneficiary. If Cedulon relies on that property, I cannot find it stated
as a required property of the rail profile.

Therefore I cannot determine from the defined evidence that the
counterparty named by the receipt is the counterparty to the offer and
settlement being audited.

8. I cannot establish "what bytes were delivered" from the Dispute
   Evidence Bundle as defined.

The Introduction says the audit layer gives a machine-checkable answer
to:

"what bytes were delivered?"

The Trade Manifest commits to the expected bytes through
acceptanceCriteriaHash.

The Spend Receipt does not contain a delivery hash.

The Dispute Evidence Bundle then consists of the manifest, receipt, and
a delivery hash, but I cannot find a signature binding that delivery
hash to this receipt or transaction. The optional payee
countersignature signs the receipt, which also does not contain the
delivery hash.

So an auditor can verify a signed offer, a signed spend receipt, and a
hash supplied alongside them. I cannot see what establishes that the
third value was the hash of the bytes actually delivered in that
transaction.

Therefore I cannot derive the Introduction's delivery claim from the
evidence bundle. Either the claim is narrower than that wording, or the
observed delivery hash needs a signed transaction binding.

One question rather than a failure.

The Introduction also says Cedulon answers:

"was this spend allowed by policy"

I am unsure which claim is intended there.

Is the auditor intended to independently verify the PDP allow that
authorized the settlement, or to verify the Receipt Issuer's signed
assertion that the spend passed its gate under the stated policyHash?

As currently defined, I think it does the latter.

The Decision Token contains requestHash, policyHash, expiryMs, nonce,
and singleUseId, with requestHash covering the six request fields. The
Spend Receipt does not contain requestHash, singleUseId, a Decision
Token hash, or tool, and the verification algorithm does not consume a
Decision Token.

That can be a deliberate trust boundary. What made me stop on it is
that Cedulon explicitly does not trust the Receipt Issuer alone for
completeness, hence the authenticated rail extract, while I could not
find the equivalent statement that authorization in the retrospective
audit is an issuer assertion rather than independently verified PDP
evidence.

If the intended answer is the issuer assertion, I think stating that
boundary would resolve the question. If the intended answer is
independent verification of the PDP allow, I think a binding to the
Decision Token is missing.

Three mechanical points.

Appendix A makes its vectors normative on their own terms, but the
receipt vector carries policyHash = "aa" while Table 3 defines that
claim as lowercase hexadecimal SHA-256. I reproduced the vector as
signed. A validator enforcing the claim definition rejects it; a
decoder treating it as an unrestricted tstr accepts it.

T8 contains both MUST-T8-9 and MAY-T8-9, although the document defines
k as the sequence number within that threat. Those are two different
requirements sharing the same T8 sequence number.

The RFC 8785 lone-surrogate paragraph says an implementation MAY emit
the escaped form instead of failing. RFC 8785 Section 3.2.2.2 says
occurrences of lone surrogates MUST cause a compliant JCS implementation
to terminate with an error.

Those are my first failures on -04. I stopped here rather than turn this
into a prose review.

Best,

Tiago Pinto
Independent author and researcher, verifiable trust and AI governance 
https://donttrustverify.pt