Radicle is a peer-to-peer, local-first code collaboration stack built on Git.
Disclosure of Replay Attack Vulnerability in Signed References
30.03.2026As announced in the release notes for Radicle 1.7.0, that version contains a fix for a security vulnerability. Radicle 1.7.1, and Radicle 1.8.0, which were released since, also contain this fix. We did not disclose the vulnerability at the time of releasing Radicle 1.7.0 in order to give users time to upgrade, before we disclose the security vulnerability in detail, which is the purpose of this post.
First off, we would like to offer our heartfelt thanks to our community member Felix Bargfeldt, online also known as Defelo. š He made us aware of the vulnerability and explained the problem in detail. We stayed in touch with Felix during the process starting from the notification up to the release of the mitigation, and Felix stayed responsive with valuable feedback throughout the process.
In this, we see confirmation of how valuable our community is and how it shapes Radicle, even when it comes to complex issues at the core of the system. It is evidence for the power of free software, not just as code bases but as community efforts.
Timeline
| Date and Time | Event |
|---|---|
| 2026-02-14T23:43Z | Felix notifies us about the security vulnerability. |
| 2026-02-14T23:51Z | We reply to Felix, acknowledging the issue. |
| 2026-02-17T00:20Z | We have implemented an exploit in the form of an E2E test, and inform Felix. |
| 2026-03-05T18:47Z | We inform Felix about our plan to implement a mitigation. |
| 2026-03-05T23:13Z | Felix provides us with valuable feedback on our mitigation. |
| 2026-03-18T16:11Z | We release Radicle 1.7.0. |
| 2026-03-20T08:27Z | We release Radicle 1.7.1. |
| 2026-03-30T12:00Z | We release Radicle 1.8.0. |
Introduction to Signed References
In order to understand the vulnerability, some knowledge about Signed References is required.
However, this knowledge is not required for regular use of Radicle.
Signed References surface via rad inspect --sigrefs and rad sync status,
but are otherwise internal, and not a concept that users will have to think about much when they use Radicle.
We thus briefly explain the relevant parts of the design.
Whenever a user pushes changes to a repository via git push,
and whenever a user operates on Collaborative Objects (e.g., repository identity, issues, and patches),
associated with a repository, they āchange their referencesā.
This means that the target object identifiers (OIDs) of some Git references in their own namespace changes.
For example, consider this invocation of git push:
$ git push
To rad://z4ā¦ji/z6Mkā¦BU8Vi
87fa120...145e1e6 cool-feature -> cool-feature
Here, the user z6Mkā¦BU8Vi is working on repository z4ā¦ji and
intends to advance the branch cool-feature from
the target OID with unique prefix 87fa120 to
the target OID with unique prefix 145e1e6.
When operating on a Collaborative Object, the name of the updated reference is of the form refs/cobs/<typename>/<id>, but the principle is the same.
A reference is updated.
(It is possible to update multiple references āat onceā, but this is not relevant with regards to the vulnerability.)
This update of a reference is recorded in what we call the Signed References of the user.
In the corresponding bare repository in Radicle storage, i.e. in our example $RAD_HOME/storage/z4ā¦ji,
the reference refs/namespaces/z6Mkā¦BU8Vi/refs/rad/sigrefs targets a commit which encodes the current state of the userās references.
Think of this commit as a snapshot of all Git references and all Collaborative Objects at some point in time.
This commit is internal to Radicle.
Its commit message and contents are not user-controlled, but computed by Radicle.
The contents of this commit are as follows:
$ git -C $RAD_HOME/storage/z4ā¦ji ls-tree refs/namespaces/z6Mkā¦BU8Vi/refs/rad/sigrefs
100644 blob d5ā¦bc refs
100644 blob 6cā¦0a signature
refs is a text file.
Its contents are in the same format as the output of git show-ref,
which lists, line by line in lexicographical order, all references with their respective target OID:
e4ā¦f0 refs/cobs/xyz.radicle.id/c9ā¦d9
e1ā¦6a refs/cobs/xyz.radicle.patch/f2ā¦f4
87ā¦20 refs/heads/cool-feature
83ā¦40 refs/heads/main
b1ā¦3a refs/tags/releases/0.9.3
signature is a binary file which contains an Ed25519 signature over refs,
performed with the userās signing key.
As the user intends to change the target for refs/heads/cool-feature,
a new commit of the above shape is computed, and the reference refs/namespaces/z6Mkā¦BU8Vi/refs/rad/sigrefs
is advanced to point at it.
The purpose of the new commit is to record the new state of the references of the user,
after applying the user-intended changes to the user-controlled references.
The (shortened) diff for refs implied by the above example looks like this:
-87ā¦20 refs/heads/cool-feature
+14ā¦e6 refs/heads/cool-feature
The history of these commits thus makes up all changes the user made to their references,
starting from their earliest change (which might be repository initialization, if they did initialize the repository) to their latest, like advancing cool-feature.
During the process of fetching this history, only fast-forward changes to this history are accepted.
Replay Vulnerability
Versions of Radicle prior to 1.7.0 were vulnerable to replay attacks.
It was possible to pick any previous refs file, together with the corresponding signature file,
out of the victimās history, and forge a new commit as a child of the latest, legitimate commit made by the victim
without the victim knowing or agreeing.
The forged commit could be shared with other nodes among the network, and such changes, unintended by the victim,
would be indistinguishable from intended changes.
This was possible since the signature was only made over refs,
which does not include any kind of nonce or further replay protection.
One possible attack would be to replay the earliest refs in a victimās history, whenever they advance their refs/rad/sigrefs.
The effect would very likely be that their repository appears empty and stale, as most repositories start out with just an initial commit.
Another possible attack would be to replay refs at a state where the repository contains a security vulnerability,
to prevent dissemination of a mitigation.
These attacks can be performed at scale.
Mitigation
A widely known measure to prevent replay attacks is to use cryptographic nonces.
There are two immediate questions to answer:
- Where to āputā the nonce?
- How to generate or compute the nonce?
Where?
Within the team, we discussed three proposals mitigate.
Nonce in New Blob
One proposal we considered was to add another blob to the aforementioned shape of commits.
This blob would contain the object ID of the refs blob, and the nonce.
It would of course also be signed using the signing key of the respective user that controls the namespace.
Such a design would have introduced more complexity into the verification process.
Signed Pushes
We did consider to immediately implement a new component to succeed Signed References based on signed pushes. Such a change would have meant introducing a new component, at the core of Radicle, just like Signed References; which is a complex task. We quickly realized that this is likely the better alternative to Signed References in the long term (more on that below), but that it would be very risky to implement under time pressure, as we wanted to secure the network as quickly as possible.
Nonce in References
Another proposal we considered was to add a new internal reference,
right into the list of references into the refs blob.
This āinternalā reference would not be user-controlled, but its target would always be the value of the nonce.
The drawback of this design is that it conflates two concerns,
or rather, two kinds of data:
User-controlled references and internal references.
Both would appear next to each other in refs with no way to distinguish the two cases,
other than the name of the reference, which in the case of user-controlled references is, also, user-controlled.
This design is less clean in a sense.
Such a decision was made in the past already.
In commit 989edacd564fa658358f5ccfd08c243c5ebd8cda,
which was released as part of Radicle 1.1.0,
the reference refs/rad/root was introduced.
At that time, the reason was to prevent so called āgraft attacksā,
which we will not go into detail here.
Decision
With new ideas to improve Signed References on a more pervasive way,
and the pre-existing refs/rad/root mixup, we decided to implement
the third design proposal, that is, to add an internal reference.
What?
In our case, to simplify, including a nonce in refs (āsigning over a nonceā) would mean to sign over data that is never re-used.
Using a cryptographically secure source of randomness is often regarded as a sufficient condition for obtaining nonces,
and this way of obtaining nonces is very common.
The chance of reading the same random data twice in that case is vanishingly small.
However, it is not a necessary condition for a nonce to be random in that sense.
The important property is that the nonce is never re-used.
Also, in many other session-based protocols, it is also important that the nonce is not guessable.
In our case, no sessions are involved.
We realized that realistically our design space for a nonce in the refs blob
is very constrained.
Backwards Compatibility
Mitigation of such a vulnerability āin a voidā, is straight forward, when disregarding backwards compatibility. Implementing a backward incompatible change would have meant that we would have to implement migration tools, and require action by all users. It was thus pretty clear to us from the beginning that we would aim for a mitigation that is backwards compatible.
The crucial design constraint we ran into was that in earlier and widely deployed versions of Radicle fetching fails. The failure was due to a check for dangling references, i.e., references that target OIDs which do not resolve to an object, during fetch. This ruled out the possibility to abuse the target OID of the newly introduced reference to a random number.
However, we are lucky! With the exception of the root commit, there always is one object that we can use as a target, and whose value is a hash over all previous history. It is the OID of the previous commit in history, in Git also called the āparentā. Note that commits can have any positive integer number of parents. If that number is zero, we call the commit a root commit. If the number is greater than one, we call the commit a merge commit.
The fact that for the root commit we cannot point at the parent is not problematic, since there also is no prior history that might be replayed.
Thus, in commit d3bc868e84c334f113806df1737f52cc57c5453d,
we introduced the new internal reference refs/rad/sigrefs-parent.
It may only be present in refs if there is some prior history, i.e.,
if the commit is not the root commit.
If it is present, then its value must match the parent header of the commit that contains the ref blob.
Upsides of this Design
When considering the complexity of the verification mechanism, using the commit ID of the parent has advantages compared to the use of a random number. When using a random number, to be sure that it was not used before, and thus to detect an attack, all random numbers used previously must be compared. This translates to a traversal of all prior history in the namespace, which is expensive. The cost to do so grows linearly in the length of the history. On the other hand, the hash of the parent commit is known locally, with constant overhead, no matter how large the history grows.
Downsides of this Design
Earlier versions of Radicle do not know about this new internal reference, and will thus treat it just like a user-controlled reference. This might cause confusion, because the user never explicitly creates or modifies the reference.
Scanning the Network
To assess the impact of this vulnerability, and improve our understanding how widespread attacks might be on the network, we developed a dedicated scanning tool (see rad:z3zzcqqBGP1NdvvMbSDt2jYYp9jSB).
The scanner operates on Radicle storage of a node.
It traverses the history of Signed References (refs/rad/sigrefs), across all repositories and their namespaces in storage.
Specifically, it looks for evidence of replay attacks by detecting sets of commits that all refer to the same state. We want to select, out of one history pointed at by refs/rad/sigrefs, all commits that refer to the same refs blob.
We call a collection of commits that all refer to the same refs blob a cluster.
Naturally such clusters can be identified by the combination of Repository Identifier and Node Identifier,
which narrows down the history it was found in, and the blob ID of the refs blob that all commits, that are members of the cluster.
Note that empty clusters are not meaningful in our context,
and that clusters of size 1 are of no particular interest to us.
We are interested in clusters with a size of at least two,
which is the smallest kind of cluster that could be an attack.
False Positives
Also note that this analysis is prone to false positives.
Seeing a cluster is necessary for an attack, but not sufficient.
It is possible for users to legitimately arrive at the same state twice.
For example, assume that HEAD is not behind rad/main and that there are no other changes to the namespace in parallel, then this simple sequence of pushes would lead to at least one cluster of size at least two:
git push HEAD:main
git push HEAD^1:main
git push HEAD:main
Further, note that the scanner does not perform any cryptographic verification of the
signature in the signature blob.
This makes the scanner much faster, and may only increase the number of false positives.
As Radicle storage is under control of Radicle tooling, which of course does verify
signatures, we chose to trust that the data in the store was not otherwise tampered with.
Scope of Our Scans
We scanned our two nodes iris.radicle.xyz and rosa.radicle.xyz at 2026-03-30T08:30Z. These two nodes are very well connected to the rest of the network, and they have a permissive seeding policy. Thus, they provide a good view of all public repositories on the main network.
Results
| Measure | iris | rosa |
|---|---|---|
| Repositories scanned | 7 090 | 7 073 |
| Repositories containing duplication | 199 | 199 |
| Namespaces scanned | 9 212 | 9 174 |
| Namespaces containing duplication | 228 | 228 |
| Duplication clusters | 477 | 476 |
| Maximum cluster size | 24 | 24 |
| Median cluster size | 2 | 2 |
| Average cluster size (nearest integer) | 2 | 2 |
A histogram of cluster sizes on iris.radicle.xyz follows. The data for rosa.radicle.xyz differs, as expected, only marginally (see table above).
| Size | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 21 | 24 |
|---|---|---|---|---|---|---|---|---|---|
| Count | 421 | 36 | 6 | 7 | 3 | 1 | 1 | 1 | 1 |
The larger a cluster, the less likely it is that such a cluster is the result of legitimate activity.
Inspecting Duplication Clusters in Your Namespace
Show all clusters of size greater than two.
Data is shown in YAML to save some vertical space. The structure of keys is RID ā NID ā Object ID ofrefs.
The values are the object IDs of commits in the cluster.
For example, the first two clusters in the listing below are in repository
z2ā¦4C and namespace z6Mkā¦5Tkm. The duplicated refs are
identified by 10ā¦d9 for the first cluster of size 3, and
by 8dā¦d8 for the second cluster of size 3.
data:
z22qZYU1Rozc61i8kT7RCt3Wmwa4C:
z6MktwkohCx8aHZ1QCjVZUiLmX92oPZFxRiFZkbq32Tk5Tkm:
10fc2cfb880bb7e2a96df378cd4869747b35ced9:
- 020d2110c8ec5ce0ccfc9cdce88f639fe094ee0e
- 3996116f2628af1033d48ec20ebd656b0c2a4922
- b68d4e6795ba13a5380e476766f82c6c61cef17e
8d901f488e1a68bab5383619da8126c34af7aad8:
- e2973b023e5b93daf24cbd45fc4dce640dae26c7
- 8fb4cd4f89b1d632a937c4479dbb9398f05345f1
- ff0ec7352ed28b922e286dc991c928724f649187
z233GtFMEuSTU35DQ7Ya8GuCD7TVr:
z6MkuvmuRwL2XVB3zDFZQpiY2fxnr6rp4Q1L61uZ1RDTk9Nf:
5a068717317eba995abd2e5fe423dfbfae60c8f7:
- fa112a588c8bdf34ff255fc43272b63505e45d29
- 46f81c05eb5e0454e21487d44782d8c28201f0d5
- 63a8a6947b30b48133be5efefd65b3fde9122780
- 5af4f25a846a47d41ee1cd8968617bde531970c9
- c1e2510fb16a537a3173dacc18caab4e0a2e958a
- 9d3f0a3647e7d022b0eb06b1aee734d70e5e4448
- b809917f5ae21c3dbe4858d6d6e0302488ede004
- 7e7c85e1bbc0341ab9d24c369043d31a626285a0
- 9885c53ce0a5212aa0c44eac591fabfc9b3e6d1d
bacac318f72171920749ce6d8ae6679a0557cf0b:
- 675e4186f0324c15ec7f0e41f0f5508b2e4a9407
- ca70fb259845a75e720971ac40fce70502732b8a
- 2c633473b1789024ae3e4074cb238850ddddcc55
- cf823b6ae75528650a5f3385446ed8d9d9cc83f7
- 0286b3d098d55f39b598a80e9151aea1b6555dbe
c45eede36f0f5ddf54c95cdc392e3d43d1a2184a:
- 599ab3267c6dc371c5a3d623df1c6a6f4ae1208d
- 59504a6ab0d1e409fd59746ac12ca6cde2450d1d
- 33c4e6fec3cefdda7ab53ce546d0767ab033b813
- 8311f777e544168bcb402906897a115334bc2210
- d55539ba6a11f6bcf1881b411555fb277e837cd2
- 9e92764db881fd77f5bf09a1f4c4184573ab00e1
- 08e6c1ff9c3e46b3a949df9f9b567f9246aceb7a
- 0d885934bf59df743cde21fd110320f1ced0faeb
- 94af66ae97bd7b31b1192957467b4b23a9f8e131
- e419d91c3c0deedb7615875cef629f20d69d83fa
- c96144f14d32a62b43631f0d5b1a0a8b4ec81c25
- 8fa0c1b0c4c46f053efbf683102f5d669fc8c587
- 9774ac4729cfbd487fdc3b6dc3e91fb09d2c3d9a
- 3e188979ed34d546d6a247a9537ed540d3f2852f
- aacb4bd22068d64c5834720911d6c1468ccbd1c2
- 893f75739b782a1190fc35d1d20be08217f5132f
- 477ee900e592af7625921beede080e1cdd7d0c7c
- 589ddd8b56a8a9540a8316f624d1e44f3c84ab0c
- 68e1e31c4be1f8a9ca6ad6b6c36387893079f17c
- 395deda862561ccf15dad3553ea851e27184fc21
- a8ef1db64223235074ed0e633e4336c9db433b47
- 95e9e3ad4fd68d23d994d120f5e422eb93e5af50
- fd65fb4c2e66dd3b672419d82dfb1d9bfb10a054
- dff86f8e623b5964bd4fa87459b46fd7c3d73ce0
fc2f3d44f50e4f0558a4911812d8863fab8473bc:
- f8a692f98629ccd366859b65ce4c1ab3dd99b39b
- e2decf4023e5d0f8f6a1d36ba5a0f20f69f54d46
- 8be1619100b5b08c22e582664fb7bf2590dceb62
- 745b0c26a85bb599970f43a05f0e805e270c754f
z292aD8q3r8xqhJvLe2zv1b24gu7a:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
54d0afcb118591ba1f45e822787ee149289da62f:
- fc98f114b5e9f47356dede7a11f37deba2153a85
- a536df85542a4e0a6b115d604db32c26d502f7d9
- b15e19bcd897cbcc2d12e35671ae6cc4a6f1ae19
z2DJwn8E33K8RVGPFzJwCmuB8rCGP:
z6Mkondm5dgAxsQnV3iuvZBCGMAQgfAg4zSdJWoNVJr7i7wp:
57d28bd65223769563a12eb7bf81176f70a83461:
- 743aa52dd80abd549a557809cba1a9809bcd4a30
- 6bd29cecfa07f32f0e339e2a4bb4b7329362a03a
- 08c6e6cc77a5af5e44c09c133f8eb0f44428af7e
z2G17GyYnKvPja34T4fWjiT6Bmrm2:
z6MksoRnvY29wWrgpGaeg5ztVAy1D3xVNjC1n5mFwuWPtwDz:
56fb7f200d7e03def9b32ca538d022c96e1fc00c:
- a021902e967d3270c366632b0cf797f847b4aa51
- c40d455ed716f47bd39ed55511bd534344a96a71
- fcac32e023de0e8702be6a3eb98dfb9ecf1bbb06
- 7d576445429711b6e04d6cde77a0c9d644f6c18e
z2M2jmXxycq7i2Yqx6PjidxUgdGDe:
z6Mkondm5dgAxsQnV3iuvZBCGMAQgfAg4zSdJWoNVJr7i7wp:
afb3c989b0a77c2fa39fc337adb336633e12a3fc:
- eff038cb188c5f5eef8c91c3e915bddd9ee765fa
- fdc35f8432464210ffd214f8bb783e9582b69792
- afce8df50881ced72cf91678c994d35122df9c0f
z2SWNmqFhpqKCwBCFLjosCnjny5b1:
z6Mknjohi1pApYKAt8XeZzjwiWSqhG8bJS77ZT8pcyTrj9ir:
e4792a846a12e85cacca51153081320b357101a5:
- 6089f604ef6f385c2fbebcef452d3ff6796c82cd
- 9060e7da5f0c85bf308dc69f9edfaba0435f1aad
- 167bf94bb8d0bf4368d2bfc0381c4425191047c9
- cd24cb2f721e7d35d729dcaf8d6d4bb200a89d30
- 4d2a29ee2186ce0bda2110cf4dee4a3840dc7b05
z2SXkaceJw3YmS89T1xGysnFSjWsw:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
17962e342eb08d3380f08bf9e0c084749d8e6423:
- b980d0dc074e61b69c73f09ff3e110a3e272a7d9
- fcc743e8908c61def5e388cdd78afcb6d793511b
- 820852767b33581f49fbe93474ff0c0e03821702
9e5f6d3ce756b7531063c34f285f625c2e45e5d7:
- 2dfae40117a6ddc69fbe68f772d9974971b5ec1e
- 675c57c5dec49cb2cbb8b23382e0f796a89207f6
- a7b733f9187ee893a361aedcdc5c284a2690e9f9
b468e088741015fbe10ebbbab3b46ed002f347f1:
- acaf9014354b97d3414b5386c3595bc6a489ec93
- 5c88f502495edd901ad4e6e79adebd8a8fea4e36
- 56b34e6a79a45e2b239f91631ba9bd97a344273c
z2UPb8WFofVxXXK1A7918M3gxe1s2:
z6MktH4iY2Mvq5ZkmoM7ayB4GM3U4p6HXKgB98nafiWkCam7:
71ec6ed930d93e4eb17aa8b97fb6cb683941ce00:
- 51bc3f33cb677b6714f846f09cbf80ce31d4ba3b
- 1496341c0c335ef4da53d409cc22658c5795c050
- f86f63915f942c8fbf4d858d7cd17305ed450fc2
z2a7Te5b28CX5YyPQ7ihrdG2EEUsC:
z6MkiuWYPxbojmhUiGGWrzSuJK3HDpbbtWo3QMm3e9VP2gJP:
aa0319d9e647a13ee9d8dd4339d0e23faba7701e:
- d6c032e15173e7c803343989f107126d187acd65
- e8e506ae8025f7b335b99b8cd4f88359b16ad786
- 8c953561309289057af43d36866bc1b211f07b5e
- cf20913f07da4da0ad99e3439ee5b450c1c7d410
- c4a60d727d45851bb8ab998b0efb4742745a8934
- 6f9e7185a80c9ea2cccafc31bd0bebdf9c8d3ca9
z6MksH6Yr4pkJqPYnY4N5e5a5bCdyCW88grKRkkK6KeMGwLN:
6c83cd06fab7dd8b0c59ab0b1d268460870384e5:
- 5748faf8d6f4f4cbf31a82e92c21198f8eb1cfcd
- efb603a0570c596d81cf226930e1c33016948310
- 2844cae8e87fbda1334fcf818b8e2b63fc3d1a3e
z2tDzYbAXxTQEKTGFVwiJPajkbeDU:
z6MkiksiCWT4LDhMQK75t6y3m2gj5iQPE1nhJfJuX1wd5Uef:
4224a04869cf6abf60562c58ad793256a9e2fb5e:
- c59eedd1f496d1837629d25f94d5b110c149c1ea
- 1be90b217604b93ca89d2492b4ac0b19cc9d6efb
- a93136ea49b575065959cb0f9c416d85218f9748
z31RADNa8uNMWFep88bhenzxTR1Ei:
z6MkpC1ahHS1r9KR12LbYnoponj8swuaAEiNpvVvB9hcXhvh:
156bae94c8df83e75a00a3e07e94120c7f873a7e:
- 4bad6e268ab12e6c5e6c42972d7e3d5555d7559d
- 7339bfd5ef35cb2cbd34999658adbdb9388f79b1
- 6f3fb0a302a6f27a7dc4fe1a21a29f6292edc811
9e5254c88d36fc6dc42145c897ec242d359b6ace:
- 35fabc1232f1682ac1de036b3ae9badfac8066ed
- f5fe3e0b6c7c6a0a88c11c95ce462165a89791aa
- 4ca5aa550a6a10e7125e3478bc6102df6a1b5586
z3bBVej6SJXuahv5hCXhiLKeB9N9k:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
ac1013b2f350d0b8b64b287d7e0d03ceb819161c:
- 66060f455ca37923d5f80407484de8f2d7999325
- fdac3aac66474d0d71adbd4d2a2e22c00cadcd46
- a2f8a5c0229b79c1e9bc4c18e7806239fd19451c
z3cyotNHuasWowQ2h4yF9c3tFFdvc:
z6Mkgom1bTxdh9fMFxFNXFMw3SbXnma6NARdsfcFuunurCap:
39fd416cd098ca04e92808fbda34ba6ed5ed1003:
- 6cb96494730c64e0936c4e6c81813b5fef4b041b
- 5bf7ee385158e6591a6ff5ea8281140c1ea768d3
- 322fd2ead09b974c1c179114414412b52dcd8563
z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C:
9bf2cf1021e0ca153212db5c530349e59729ad50:
- e81e336b1f89f78e8362c9c33fc659aec6ef8e38
- b4c8b0cceb0fd00215b9cd4eb58c1284c4d05eeb
- 91d69ad99564695610b111a2186781e1bdd93d71
z3gqcJUoA1n9HaHKufZs5FCSGazv5:
z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV:
27b23b3e05c6d6a4ae1d9db3e53e046989dabd0d:
- 406c96681faca1a8def7ab01184f6bcc16dd55d6
- b748c1d7cda0486fd02565bfeb556f65da8826cd
- 3708d86b4d14b34b9dd1cc2b5b3785f4bdc2f852
9167c0a5c209bf3a31adf25d59acc411e469fe70:
- e11752a3a42617b40c427292add2235db54308c9
- 420a0134ff94e9037c10bfb324cc403f01d119a1
- 69145ba62b572ff49b7c8836d8efce348c23e2f2
- f0a59d5fd4ec17d3a12b88732cb52651233b72d6
- 6f5c92b73e1756098fbe99a5e2b98fdcd1888f1e
- 4910e88b9caddb43382fea2a75ea1dd0a674f7c6
z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM:
1f3685f577c2319748cc43f2b97559e6e6f27b30:
- f0ad9600f3bfa3c193f52507480ec161be0b0cd0
- 2fc43986f24c1a8ca092186e9d179f02d4a5f288
- 979e00e332a9dff186e28bf6f2ed8cc62047b558
fb8387acc1c8cb9fe33d13ecaf01f3a5eff6e07e:
- 40361a592d391b8b56e23173605ac490eb227d94
- ab52bc081d8720216c67c2643b70779e31467199
- 729ef07fef8fc79fb2d6dcfc71d374b20095471b
z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz:
4996cff3821f7cc5b4850a56efcc887ff6983866:
- e0502b2c843d0997e43e63548388ce6884d73dd0
- 178142f005781af571830ba573019e3e57e28f62
- 7176c630b9f2cc22c8859ccfdcfd6d66da438af7
- ac7dcb92b998f9716872421f2d45faf285784940
ebf3aedc50f350e8670a1178a70c5af1217eb664:
- dee5da561a0c76ec07938f9b88047b3f3bb210c7
- d3f58fe73caeededacb892c82cc649545b1008e0
- 6d28c01d2afaf9fd0919592b2e90ea94e48e9905
- 34c04e812208f53ea43fabba3a70720b949f6c72
- a8301092c4983b672a66f0a263aa6c95b813caaa
z3i8ZmSrLFsuGFkvCsMBCSskqDipZ:
z6Mkum88mKcX2sARx8R18RZUmAdakPFbXtdHzn8JNVEDqkxj:
10ab8994984779b7746bcbfd84d837aba87d9d47:
- d8f8dcb0f40af5fae6ec61fe8223c0b0a02fba7f
- e6b58eb83fb64e662b0cb6acae2e207271fc9e4b
- 4f40235f10e11a7569b69fae658466918bc74300
- d83c5dba70548f42e53ccdbb4bb0133d1c3f16bc
z3kozTn4Kn5eJtgJQj1aCFUpqxW5Y:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
a1f828485c94c89636d5c9e22a541dcc908a203a:
- 0fecd475bd4df5cd2003f6e492d2505c7071e53f
- dc737994daff3d70f92a80aff220b1090a5ff041
- 090a873a48ad5815ce40078c10f443b87d8cdcf2
z3nSmSzzTjgtbapU2P33JDdZ9Tca:
z6MkuvmuRwL2XVB3zDFZQpiY2fxnr6rp4Q1L61uZ1RDTk9Nf:
17207437aa5606e660b95066a0e9ac7b42a1eee3:
- 9ad94184c9fc8056103a8d31dd4f3c1c1df3362a
- fa5ce57a31843137769a85aa9425fdd872e5e29b
- 507b9b36086db7e6a55a61bc67a7fe6a9312db9d
- 72e2c085aad4b34f7f5bbcb0cd89c32a9d34243f
- b4457712ae6f94886f1d3040aa93a1fa6e9661c9
- 8a4a76c132672b8d91cb5d1a9f521a1d2d36d7cf
- 9333a611f28e897afaf0290badf8db1d4c06fa8f
- 89990057bd7b6bf9f9b779c78efb995458569b14
- b5bb2f7011fb9d20a8f9e92f41070a838b48ba25
- 6b23ea763d319333257caabd2875255660ed5ab8
472d33e0d3bef33f5c5fb4914826b62d65fc7bea:
- f8b44c41c15e91041dd54bd92f8a1797d55347eb
- 021a78dc7265aab50b531829179dcf71d0d0b468
- 07bb5c12feacb35989efebf1e3d9b0f5310e65da
- 9fcc377078cca9bca17105b7ddc6297c4fe7a983
- 7b794d3d35e563258bf5d9adc0778fda06bc8197
66721d2144d0958526fa3325a5dc36b2a62dc488:
- a48104cdc5d6c932ee7bec90000f19c4a6ca49e4
- c2755f5706a240f5b95e1c9458b736799dd5eeac
- 8b0319df5b51b56c1d4325f328d3803b766370b7
- 0d882594a00d654b20528a8acdc185798becf623
- c019f8d3be7650cc13cf21f21c0282864c257e55
99f9a48b0ba538e7f27317be95bb4cdc51c49daa:
- 0db528badd5050b34051c3979e5d830f01cc3641
- bc3bbfb732ac8a29fe14d7b0e709987c8a16e254
- 66553217cf89ac330e44753637c3e4fc5143d39b
- cc5fe0d799d35b9b8435cd759dfedaef2d37224b
- b13205272467ba26553379cdaea893b81efa217b
c6ca4c1bdbaaaa2072a6cbe8a8c0dd35e17d054e:
- bf927bab4c19d1dd9ff7fa54cfe4c856c5ca59f6
- 2a47e681d9f2ea5ddd41880854ff48b907718752
- b3e532bf7173d40623d50b18003694331ee80aa1
- 2de991b41ad48faa98e1040e31370363a6640581
- 2c402f3f304f438781b15c3737a1560a0f5adc7a
- 194b61a9f6b189f608881985fd40d5e8558c2267
- 7557388731ea0423262ac4b1f6a2244e011ff475
- 6582b20dfa5f27155aab2dc9a6bc06d13f40774e
- 6a6bbc67eaeb0051acf84da47eeea4472961f152
- e3e134332b6c34eec0cc30be2cda372c6a92e3f4
- 5d0d86ee7096389a417d0bfd027a17b6a528b9eb
- 33cc59256f07423667100a85257859e777506175
- e993a8beea0af22b10123967bc7132b1711ce7ac
- 3e83e00b335a111b8dc1e014aed4e3cfb7fa368e
- 83958c8a9f43d284e0ef241e70176012a34f1959
- d78e17ef99837cecc7dc1c6207d0f00217d4d241
- 1c5f2a29b0b1b68cfe1094c2c3bd81dec907a3ee
- cdd4571c8ca88fcd3aaaf70cc2ad7ec865a0c55a
- 6a2c1ed4bdb097c1174fdec1a498b74b24bf69f5
- d1ce807a52045776ea73af97a2f9d4b7e2cbd6e6
- 294f05fc3fcc0269bf1e61cf6ed7b0997fcfe743
z3qsiV1cLueoZ7mbEAHCTA58yPP7e:
z6MkfSMUiSTtktgCbXDgjkAUcBadNzKgHrNnE6BPRC6GnNk4:
02cb4108bea242fa2dc698c66fd2d7ecf3882609:
- 13ca3c8d3808d4c2dc654e764927a80b66398c53
- ab8d6a02fe136652e1a52f04640ca92c9d091895
- 5173ae463e6614117eba2c473c53560c39f962d4
- 17267c59133cb2c6b629f51852f40cf25c6e036c
6452b1f1a37939d0e8d8b7d91c72f6244d790627:
- a4c02a390638da0a0aa0aded889caab47faa6eb2
- 5b8151b4e73e8492b88612e3c19e9a4aff37b671
- 6dd4a929c83431a7129c1c7d40015f2d574d1dbf
z3rwhJ9rQ82H6GXg7ZCt3UNpStbaW:
z6Mkg4WMCHiK1wGCXLJidM4fWQuuCtb7jVmuLqCxSEwJLtvp:
ef70e392e8afb2327318c897900a90c8d920f5d8:
- 8939508dc9cfe8f31fc06e194130a679aae9b2ce
- ad545fbbc5ac30ddcd39959bf8dbe07e7e7e9883
- f4ff9f55a20689cd9a11dd13c811dfbfe46c4823
z3xjvkTXHZpBuLUEZjEqmmcsk8HPe:
z6MksoRnvY29wWrgpGaeg5ztVAy1D3xVNjC1n5mFwuWPtwDz:
fff559359e9b22c3b309dabae53a47d0a6559d0e:
- 546bc4a6364ac2c573367707480a3c68f817f939
- 76ac736a3eff6d45812127cc7ce7a8eca77262f8
- 0b01f44d1c574ee54583519d9fed104687fdfbc0
z4P7htL76j7ag3F76gTaT1zLuZRe7:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
cfe6ba4988ea2810dc597ba9f7e7f267ff8fc476:
- 2ee7eb06e05afd9e59f9173de38106db591a91c1
- 9634b76df4de925fcc3adef4399731b6c52f75e5
- 371b13211bbbf7316a890678ae845a9810c77e11
- e770f7d340ab06314ca0af4e0f0fd03a3cf11bd5
- a7e51437bb06757807b0a59dcefd3f6230e88d5a
- 5aaf3dc86d8be531d271c73d9eb9474beebea8ed
dc10dcaa1b25378d08fc855febe81ea79c823ea0:
- 7ef26dd108bdfbb95b9e2e4a84e629c46d68274a
- 7977fa4c1d2f78ac6a4554f0fe16738fee7f2340
- 54de6e53f6303cdddc0e974f7b69b6057ac9714c
- 7c09f2fa5e3d1e311f868f9d711df3b1f34f3c85
- 8f3d5a12f84c1041ea2882fbecfae588c89b88b6
z4SmCptZ81EcBS5pbvFNbppFkY1Cw:
z6MkpZ5eyZFjPDiF3KqtS6yJmqq1gkN3KRMHx64ein5qTtb9:
459ec8af8ac81d68a961d67d7ad30e49a6627c56:
- 104f16f083d510415a280d61e8e34f457190b35d
- 45f94ce863f46333e9bb83d03daa3cac6691953a
- 09e0f91ace979849cf0a10e9431ab2dbac26df80
z4V1sjrXqjvFdnCUbxPFqd5p4DtH5:
z6MkeyYzy2PmF8RdXzDXeNzbUPmqxjPG9mgDSwZGcr5Dccbv:
c896b7e48ec5eafa181d705454034405ee0a5d17:
- d9e34d2957cfad92d6027e080f59bd9f070e81a6
- 76b44d734b28d2827d997a1680cf71d474299203
- c362cd037f247eaf393c646af0a52670a74b22c4
z4V2SLbvf46Ttgkxkuyvboc5yYbGy:
z6MksYHVGBqiS1dpnhToGMP6gMjHPnHrbH5NEqa54W32CAjk:
9c62d61f65d9d8655f8bfb3877b197f1e794e691:
- 2e0a912e0702da0591c1eb229a87e1addb326c61
- d5a0dbdf6db706729ce48c03571892e4ec6eaeb3
- e69e8ba85fbfd0b8dc7b21b7203a7247702c6a81
z5EmYRhaFMaaccNYmeufWat8x1BA:
z6MkgvaRdahQZfnf7ccMHByeHtTYrMrM3MuSGYawF6ZL6DNj:
3b74250fd2343e03f479495ccdb9e054326efca9:
- 57857ec2e1e1e782b554ddd21158076ea0744986
- 47449832294923399fdc44d875c73b43fd13c0d8
- 2548f44064824ec39f566a20e292c5caddb6f398
z8sG73WVo2TAKjoqh3vBvhfRqPsw:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
52c8f1d5c7eee9f2e40f376219f8018166a7e298:
- c6357c77874ff8e3a61ee91e247b2c5b19256223
- 79f89bf1d911fd13cb0635e5d910506417f656e4
- 20f5a8643df6d891a970e1ad60217d6bceb8cfe8
- 9404609cefe6f61a9c723d3da80b014ae7e4f4d5
a5cd8e052ee1207e9d7767e285f7b9d72ca8640b:
- b8a1b10684646f5b5c0a452faca73e45037c5cd8
- 62bd2e9431c2fba38525f75b3f92f25b8740ee23
- ec48e456ee6ae7bdb1c883650872e64d11a46926
zcmrr8AZD7dcRwAKnXBroJKyT2En:
z6MkpC1ahHS1r9KR12LbYnoponj8swuaAEiNpvVvB9hcXhvh:
641b572966b093f16d7bdf9236fc5f1328f6b221:
- af6a171c5b48ad9e1dcf00ac4de777d6b1f53f3a
- bacc1a70110fbcb293861f86f85a1c9aaca927a8
- 6fc23526bf9ad9f40f51b778dcda804f1e6eec44
zjTKEBr1jwtpu7CkJtBd6jTnCXSE:
z6MkpC1ahHS1r9KR12LbYnoponj8swuaAEiNpvVvB9hcXhvh:
0774b3680463fb175c1bbc18d5d6ada7f70e7bde:
- 40a9bc8ba4a26be70fa547ee41c05e3faab26cca
- 6015e13edb37132dc9b2064d8c35e4b737efe786
- 2cee6f9645d259fc317ae351c6d8c344b89f17de
2c2bc8da268c4f4f4390ffe74acc9108165f0ae5:
- 35db02c7d4577cbb36211e355e089ea7fbafa745
- 8f50aa62cb1bb88706623beaa85e331bc49f8db9
- 51f567a1cede0e5adb3cf016a232a105ed08a4f9
8d897c693c1818d2b3e20a3c9bc4850b3aa399b2:
- 7c52d18dbf0482342b24ca408695f8ff30b218e5
- 861e64627a58e708dbdba038be27035388710c9e
- 364ef81a3563d12d5147a3d63ebe8e15d87bd090
f20ae096292aa467e3f22765bdfdb6e6dde874f6:
- 4ac9381c78a06668314b76be1f41bb487ebc70f4
- 2d65a1b459d8c15f8dd48907da0e925d81859140
- 6b51bc85a5121985d5b5bce498580cd871d748b4
fd1ae02017faa8be202f2756d50ef96a1b35cfde:
- bca64f8cb5fe5bee3b0a80b307d5b35a01874bb8
- e5dd135f1d7f37aed5f15a4eb52983ce043e83c0
- a22eac9724b66573680d84f8d39bf406e17518f6
zoBPQV6X2FH296n9gQxJr6suvSSi:
z6MkewCavrcnLurWU4TzMUYT4kX9FSknYvbZEWbh1CnGdzaP:
f84508c2e4ea65a8779bd03981b54680322c0d73:
- ee4d6709d57c0846c9a2f39d4084d2a3e236b299
- 435c10668b909d59bc7e182ed4bb8af9ceccc8c4
- 7128946bde23df75ca4fae07c0815c27aed150c0
zu5U7GRpyZRXeHfz6K5vdPq5CVVM:
z6MkvVZk1A3KBApWJXv2Ju4H14ErDfRGxh8zxdXSZ4vACDg5:
ced794c0d15bb629df1eb3d8f3637a083dd754ef:
- 1932fb5ef1f27eea461ed042ef1fe173114e7fc1
- 7153ece0e045fce12d715ccd328f4abe990f3a9a
- 4b56ab21a4b8705e7a6ba4d2862f4a0b67903fd4
Alternatively, consult one of the files {iris,rosa}-aggregated.json in
rad:z3zzcqqBGP1NdvvMbSDt2jYYp9jSB or run the scanner on your own node.
If you need help interpreting the data, please reach out to us via team@radicle.xyz or via Zulip.
Vulnerable Versions
All versions of Radicle prior to d3bc868e84c334f113806df1737f52cc57c5453d,
which is included in release 1.7.0, are vulnerable.
Fixed Version
In Radicle 1.7.0
In Radicle 1.7.0, a major refactor of the signed references logic was written.
The base of the refactor was taking the previous implementation and performing a rewrite that allowed us to focus on the core logic.
This allowed us to quickly iterate on the above ideas,
and then implement the mitigation mechanism of including the refs/rad/sigrefs-parent in the refs blob.
As mentioned earlier, the inclusion of the refs/rad/root in the refs blob was previously introduced to prevent graft attacks.
There was a note to make its inclusion a hard error once enough time had passed.
We took the opportunity of this release to make this change as well.
If Radicle 1.7.0 would have rejected all histories that do not end in a commit with refs/rad/sigrefs-parent in their refs,
this would have also amounted to backwards incompatibility.
It would be impossible to fetch updates from nodes that have not yet updated to Radicle 1.7.0 via the network.
That would have caused lots of disruption.
Replay Detection
We cannot travel back in time and retroactively add refs/rad/sigrefs-parent to
the refs blob in the histories of all namespaces.
What is possible, however, is to detect replay attacks in old histories.
It is possible to traverse the history and look for duplicate refs blobs
(or signature blobs, as the signature is deterministic from the signing key of the user and the refs blob).
If the refs of the head of refs/rad/sigrefs is found to be duplicated in the history,
we can decide to skip this head, and try its parent.
This process can be repeated iteratively, and it is guaranteed to terminate,
since the refs blob in the root commit cannot possibly be referred to by any
earlier commit in the history; simply because there is no earlier commit.
There is one slight oddity. As alluded to earlier the user may legitimately wish to restore an earlier state. The detection mechanisms would skip such an update, and effectively drop the update.
In Radicle 1.7.1
Once Radicle 1.7.0 was released, our community was encouraged to update. Generally, we would introduce release candidates before releasing a new version. Unfortunately, due to the vulnerability, we needed to release without the usual ceremony.
When our users updated to 1.7.0, they quickly noted that there were some issues.
The first was unrelated to the vulnerability, and revolved around parsing of IPv6 addresses.
However, the second was due to the aforementioned hard error of the reference refs/rad/root
not present in the refs blob.
To provide some context, the refs/rad/root mechanism was introduced to prevent a graft attack.
Essentially, it ties the commits in refs/rad/sigrefs to the repository they are introduced in.
This reference is calculated by Radicle, and there was a code path that introduced a check for its existence,
and if it did not exist then it would be created.
This change was introduced in 989edacd564fa658358f5ccfd08c243c5ebd8cda.
Then, 10 days later, another change was introduced.
From that change (commit 09f796234d76f4a25807371bb709c18678ac7bc9) onwards the identity root would always be computed
(by traversing the history of the identity COB to its root).
Since this method would now always result in returning an OID,
our previous condition for a missing refs/rad/root would never return true.
Any repositories initialized via rad init after 1.1.0 were not affected,
but those initialized earlier, e.g., on 1.0.0, would be missing refs/rad/root.
All that said, there were a significant number of repositories that did not include the refs/rad/root reference.
Furthermore, this meant the hard error became a backwards incompatible change.
We relaxed this condition so that nodes could make progress in Radicle 1.7.1.
In Radicle 1.8.0
With 1.7.1 released, we realised that we had one more piece of the puzzle to implement. We needed to make sure that downgrade attacks are not possible, and to allow users to opt-in for more strict verification.
In order to achieve this, we introduced a new concept, which we call āfeature levelā. It describes, on an ordinal scale, which features are detected on the history of Signed References. For now, all known features are security features, so currently feature levels translate directly to security level. Also, these feature levels are strictly monotonic, i.e., higher ones include all features of the lower ones.
The three feature levels, as of Radicle 1.8.0, are:
none < root < parent
none: This is the original behaviour with arefsandsignatureblob, where therefscontain neitherrefs/rad/rootnorrefs/rad/sigrefs-parent.root: Requires the referencerefs/rad/root. Added in Radicle 1.1.0 (see above).parent: Requires the same features asroot, and additionally the referencerefs/rad/sigrefs-parent. Added in Radicle 1.7.0.
The feature level for namespaces is now printed by rad inspect --sigrefs.
This allows users to understand which feature level their collaborators are on.
The reading and verification of signed references now detects if the feature level is parent.
If so, verification can return quickly.
Otherwise, in the case that it is root or none, the history is traversed.
The traversal is expensive, as it performs a number of reads from the Git repository that is
roughly linear in the length of the history.
As more and more users upgrade to 1.7.0 and above these traversals will have to be
performed less and less, restoring loading performance of Signed References.
The results of the traversal is used for further feature detection, and also for detection of duplicate refs.
After completion of the traversal, the features can be inspected detect downgrade attacks.
For example, this would be the case if the head commit could be at a root feature level, and its parent be at the parent feature level.
In some cases, this could be a legitimate downgrade from a user upgrading to 1.7.0 and back to 1.6.1, due to the aforementioned compatibility issues.
The solution to this is to always allow the user to create a new commit with the latest feature level.
If no downgrade was detected, the verification process then takes the first non-replayed commit as the head, and loads references from it.
With the feature levels in place, we introduced a configuration option for the fetch protocol.
In the Radicle configuration file, $RAD_HOME/config.json, a new option was introduced.
The optionās key is node.fetch.signedReferences.featureLevel.minimum and its value is one of the feature level values, as string: "none", "root", "parent"; defaulting to "none".
The value is used during a fetch of a repository from the network.
The fetch will reject any namespaces that have a rad/sigrefs head commit that is below the given minimum.
That is to say, if set to none, then namespaces will be fetched, but still verified with above rules.
If set to root, then the fetched namespaces are protected from graft attacks.
Finally, if set to parent, then the fetched namespaces are protected from graft attacks, and replay attacks.
You may notice that setting this configuration value allows to trade-off security vs. backwards compatibility.
As more nodes upgrade to 1.8.0, users should update the minimum to parent, as soon as possible.
Once that is done, there is one last escape hatch for fetching and cloning.
The rad sync --fetch and rad clone commands now include a --signed-refs-feature-level option.
Its expected value is, once again, one of the feature levels,
and its behaviour is the same as the above configuration option.
Roughly speaking, by increasing the feature level to root, one can expect
backwards compatibility to 1.1.0, and by setting parent, one can expect
backwards compatibility to 1.7.0.
Recommended Actions
- Update to Radicle 1.8.0 as soon as possible.
- Wait until other users, especially those that you collaborate with, have updated to Radicle 1.8.0.
Use
rad inspect --sigrefsper repository to assess. - Edit your configuration with
node.fetch.signedReferences.featureLevel.minimum = "parent"for improved security.
Seed Node Operators
Because, to the best of our knowledge, there are no replay attacks happening on the network, we think that no immediate action other than updating to Radicle 1.8.0 is necessary.
However, please familiarize yourself with the newly introduced concept of a āfeature levelā (see above).
Consider to set node.fetch.signedReferences.featureLevel.minimum = "parent" after a reasonable period of time to allow others to update.
The Radicle team will do this for seed.radicle.xyz within days, and for iris.radicle.xyz and rosa.radicle.xyz
within the next weeks.
If however, we notice an uptick in suspicious behavior on the network, we will considerably accelerate these timelines.
The Future
Monitoring the Network
We will use the scanner mentioned above to continuously detect potential attacks, and extend it to also understand how many nodes have updated to Radicle 1.8.0.
Automated Cross-Version Testing
One take away from the process of implementing and testing mitigations, and their impact on compatibility, was that we will continue to improve our automated cross-version testing capabilities. This will help reveal issues before we release.
Signed References
Signed References are a design that was conceived some time ago before Radicle reached 1.0.0. We think that a similar solution, based on signed pushes and storing push certificates would be more sustainable. The Linux kernel project, for example, provides transparency logs based on this format, and it is not specific to Radicle, but defined and maintained by the Git project.
Our goal is to have future versions of Radicle to support both Signed References as well as push certificates, to allow for a large time-window of cross-compatibility. Of course, removing support for Signed References means a breaking change down the line.
Acknowledgements: We would like to thank maninak and rudolfs for their helpful comments on drafts of this disclosure.