Radish alpha
r
rad:z371PVmDHdjJucejRoRYJcDEvD5pp
Radicle website including documentation and guides
Radicle
Git
post: Disclosure of Vulnerability in Signed Refs
Lorenz Leutgeb committed 1 month ago
commit a489f7b5409d2bbe047840eb121d73cf07362c2f
parent 490311e
4 files changed +1396 -41
modified _posts/2026-03-18-radicle-1.7.0.md
@@ -29,7 +29,7 @@ curl -sSLf {{ site.url }}/install | sh -s -- --no-modify-path --version=1.7.0
## ⚠️ Security Fix

This release contains a security fix, and so it is **highly recommended that you update all of your nodes**.
-
The information on the vulnerability will be disclosed on the 2026-03-23, with a full write-up at <{{ site.url }}{% post_url 2026-03-23-vulnerability-disclosure %}>.
+
The information on the vulnerability will be disclosed on the 2026-03-30, with a full write-up at <{{ site.url }}{% post_url 2026-03-30-disclosure-of-vulnerability-in-signed-references %}>.

We have taken the time to scan all existing repositories on our public seeds, and have not detected any active exploitation of the vulnerability with malicious intent as of today, 2026-03-18.

deleted _posts/2026-03-23-vulnerability-disclosure.md
@@ -1,40 +0,0 @@
-
---
-
title: "Vulnerability Disclosure"
-
image: radicle-1.png
-
redirect_from: /2026/03/23/vulnerability-disclosure
-
---
-

-
As announced in the release notes for [Radicle 1.7.0]({% post_url 2026-03-18-radicle-1.7.0 %}),
-
that version contains a mitigation for a security vulnerability.
-
Due to backward compatibility issues, indirectly related to the mitigation,
-
we released [Radicle 1.7.1]({% post_url 2026-03-20-radicle-1.7.1 %}).
-
That release restores backward compatibility and also contains the mitigation.
-

-
With the knowledge of how 1.7.0 broke backwards compatibility, however, we decided to
-
take a slightly more general view on backwards compatibility in light of the security
-
vulnerability.
-

-
We are currently working on two features (see below) that will be released in version 1.8.0,
-
and decided to delay disclosure of the security vulnerability until that release is available,
-
at which point this page will be updated.
-

-
Radicle 1.8.0 will better protect users, and give them options to increase their level
-
of protection further via configuration.
-

-
## Downgrade Attack Protection
-

-
We are adding protections against downgrade attacks that rely on data that was received per-node,
-
rather than requiring all nodes to upgrade in order to stay compatible.
-

-
## Configuration of Protection Level
-

-
We are working on a configuration option that will allow node operators and users to
-
decide how backwards compatible their node should behave.
-
That is, a way for node operators to decide for themselves where they stand on an ordinal
-
scale that trades off maximal backwards compatiblity and minimal security one end and
-
minimal backwards compatibility and maximal security on the other.
-

-
---
-

-
We would like to thank you for your patience and your trust as we work on ensuring that the
-
Radicle network stays secure.
added _posts/2026-03-30-disclosure-of-vulnerability-in-signed-references.md
@@ -0,0 +1,894 @@
+
---
+
title: "Disclosure of Replay Attack Vulnerability in Signed References"
+
redirect_from: /2026/03/23/vulnerability-disclosure
+
image: radicle-1.png
+
---
+

+
As announced in the release notes for [Radicle 1.7.0]({% post_url 2026-03-18-radicle-1.7.0 %}),
+
that version contains a fix for a security vulnerability.
+
[Radicle 1.7.1]({% post_url 2026-03-20-radicle-1.7.1 %}), and
+
[Radicle 1.8.0]({% post_url 2026-03-30-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`:
+

+
```shell
+
$ 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:
+

+
```diff
+
-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 [nonce]s.
+

+
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
+

+
<details>
+
  <summary>Show all clusters of size greater than two.</summary>
+
Data is shown in YAML to save some vertical space.
+
The structure of keys is RID → NID → Object ID of <code>refs</code>.
+
The values are the object IDs of commits in the cluster.
+

+
For example, the first two clusters in the listing below are in repository
+
<code>z2…4C</code> and namespace <code>z6Mk…5Tkm</code>. The duplicated <code>refs</code> are
+
identified by <code>10…d9</code> for the first cluster of size 3, and
+
by <code>8d…d8</code> for the second cluster of size 3.
+
  <pre>
+
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
+
  </pre>
+
</details>
+

+
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](mailto:team@radicle.xyz)
+
or [via Zulip](https://radicle.zulipchat.com/#narrow/channel/369873-Support).
+

+
## 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 a `refs` and `signature` blob,
+
  where the `refs` contain neither `refs/rad/root` nor `refs/rad/sigrefs-parent`.
+
- `root`: Requires the reference `refs/rad/root`. Added in
+
  Radicle 1.1.0 (see above).
+
- `parent`: Requires the same features as `root`, and additionally the
+
  reference `refs/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
+

+
 1. Update to Radicle 1.8.0 as soon as possible.
+
 2. Wait until other users, especially those that you collaborate with, have updated to Radicle 1.8.0.
+
    Use `rad inspect --sigrefs` per repository to assess.
+
 3. 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.
+

+
[Defelo]: https://defelo.de/
+
[nonce]: https://csrc.nist.gov/glossary/term/nonce
+
[`git show-ref`]: https://git-scm.com/docs/git-show-ref
+
[Collaborative Objects]: https://radicle.xyz/guides/protocol#collaborative-objects
+
[signed pushes]: https://people.kernel.org/monsieuricon/signed-git-pushes
+
[push certificates]: https://git-scm.com/docs/pack-protocol#_push_certificate
+
[`rad:z3zzcqqBGP1NdvvMbSDt2jYYp9jSB`]: https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z3zzcqqBGP1NdvvMbSDt2jYYp9jSB

\ No newline at end of file
added _posts/disclosure-of-vulnerability-in-signed-references.md.bak
@@ -0,0 +1,501 @@
+
---
+
title: "Disclosure of Replay Attack Vulnerability in Signed References"
+
image: radicle-1.png
+
redirect_from: /2026/03/23/vulnerability-disclosure
+
---
+

+
As announced in the release notes for [Radicle 1.7.0]({% post_url 2026-03-18-radicle-1.7.0 %}),
+
that version contains a fix for a security vulnerability.
+
[Radicle 1.7.1]({% post_url 2026-03-20-radicle-1.7.1 %}), and
+
[Radicle 1.8.0]({% post_url 2026-03-30-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 codebases 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-30TXX:XXZ | 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 users push changes to a repository via `git push`,
+
and whenever users operate 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 namespaces change.
+

+
For example, consider this invocation of `git push`:
+

+
```shell
+
$ 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 shape `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 repo 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 users' 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/… 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 lexicographic 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 users' 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:
+

+
```diff
+
-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.
+
That is, attackers could use `refs` and `signature` from an earlier commit of the victims' references
+
to forge a new commit, 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.
+

+
It was possible to take any previous `refs` file, together with the corresponding `signature` file,
+
and forge a new commit as a child of the latest, legitimate commit made by the victim.
+

+
One possible attack would be to replay the earliest `refs` in a victims' 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.
+
This attack can be performed at scale.
+

+
## Mitigation
+

+
A widely known measure to prevent replay attacks is to use cryptographic [nonce]s.
+

+
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 respective user that controls the namespace.
+
Such 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 change would have meant to introduce a new component,
+
at the core of Radicle, just like Signed References,
+
which is a complex task.
+
We quickly realized that this likely is 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 `refs` blob,
+
This "internal" reference would not be user-controlled, but always target at the parent commit (if there is a parent that can be targeted).
+
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 case of user-controlled references is, also, user-controlled.
+
This design is less clean in a sense.
+

+
Such decision was however 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 vulnerability "in a void",
+
disregarding backwards compatibility with already deployed nodes in the network,
+
is straight forward.
+
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, if one of
+
the objects targeted by a reference cannot be fetched.
+
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 it's 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 cluster can be identified by the combination of RID and NID,
+
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, share.
+
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-26T22:26Z.
+
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 032 | 7 015 |
+
| Repositories containing duplication    |   199 |   199 |
+
| Namespaces   scanned                   | 9 140 | 9 101 |
+
| 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 | 24 |
+
|-------|----:|---:|--:|--:|--:|--:|---:|---:|
+
| Count | 421 | 36 | 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
+

+
In `rad:z3zzcqqBGP1NdvvMbSDt2jYYp9jSB`, you can find the results of our scan.
+
To find to find out whether one of your namespaces contains a cluster of size
+
greater than two, consult one of the files `{iris,rosa}-aggregated.json`.
+

+
If you need help interpreting the data, please reach out to us [via team@radicle.xyz](mailto:team@radicle.xyz)
+
or [via Zulip](https://radicle.zulipchat.com/#narrow/channel/369873-Support).
+

+
## 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 opporunity of this release to make this change as well.
+

+
#### 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` to verify is found 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 possible be referred to any
+
earlier commit in the history, simply because there is no earlier commit.
+

+
There is one slight oddity, and that is, as alluded to earlier, the fact that the
+
user may, legitimately, wish to restore an earlier state.
+
The detection mechanims would skip such 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 update to 1.7.0, they quickly noted that there were some issues.
+
The first was unrelated to the vulnerability, and revolved around IPv6 parsing.
+
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 `09f796234d76f4a25807371bb709c18678ac7bc9`.
+
This would *always* compute the identity root (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.
+
This is only a problem for repositories that were created before the initial change,
+
and did not see an update in this 10 day window.
+

+
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.
+

+
These improvements were made and 1.7.1 was released at 2026-03-20T08:27Z.
+

+
### 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.
+
This was achieved by implementing feature detection for signed references.
+

+
There are three feature levels for signed references:
+
- `none`: This is the original behaviour with a `/refs` and `/signature` blob,
+
  where the `/refs` contain neither `refs/rad/root` nor `refs/rad/sigrefs-parent`.
+
- `root`: This implies the behaviour of `none` and includes `refs/rad/root`.
+
- `parent`: This implies the two previous levels and includes `refs/rad/sigrefs-parent`.
+

+
The reading and verification of signed references now detects if the feature level is `parent`.
+
If so, we can verify the signature and confirm we have a cryptographically secure commit.
+
In the case that it is `root` or `none`, a walk of the commit history must be done.
+
This walk does further feature detection, and also keeps track of duplicate signatures.
+

+
When the walk is complete, the features can be inspected to note if a downgrade attack was attempted.
+
For example, 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.z`, due to the aformentioned 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 attempted, the verification process then takes the first non-replayed commit as the head,
+
ensures that the commit is verified, and returns those signed references.
+

+
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"`.
+
In the following, we omit double quotes when talking about feture levels.
+
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 this value is a trade-off between security and 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.
+

+
#### Shields up!
+

+
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.
+

+
Instead, we decided to give the user more control.
+

+
Firstly, 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
+

+
The feature level `root` refers to `refs/rad/root`, which was added in
+
Radicle 1.1.0 (see above), and the feature level `parent` refers to
+
`refs/rad/sigrefs-parent`, which was added in Radicle 1.7.0.
+

+
On top of this concept, we implemented a mechanism for users to choose which
+
feature level they consider acceptable. This value can be set in the 
+
configuration file, as `node.fetch.signedReferences.featureLevel.minimum`.
+

+
The default value is `none` in order to maximize backwards compatibility.
+

+
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.
+

+
We recommend all users to update to 1.8.0 as soon as possible, so that
+
more and more nodes in the network can then choose to configure a minimum
+
feature level of "parent" for improved security, which we strongly recommend everyone to do as early as possible.
+

+
## Recommended Actions
+

+
Update to Radicle 1.8.0.
+

+
### 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 of 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.
+

+
[Defelo]: https://defelo.de/
+
[nonce]: https://csrc.nist.gov/glossary/term/nonce
+
[`git show-ref`]: https://git-scm.com/docs/git-show-ref
+
[Collaborative Objects]: https://radicle.xyz/guides/protocol#collaborative-objects
+
[signed pushes]: https://people.kernel.org/monsieuricon/signed-git-pushes
+
[push certificates]: https://git-scm.com/docs/pack-protocol#_push_certificate

\ No newline at end of file