10
45
Fork
You've already forked doipjs
28

Keybase proof not verifying #63

Open
opened 2023年11月14日 22:33:55 +01:00 by aspensmonster · 6 comments
Contributor
Copy link

See the Matrix chat for context, reproduced here:

nicfab:

Hi,
My profile is https://keyoxide.org/9a85d44ae2d38f19d5ffe2c8655812fca35b817f, but - as you can see - on Keybase, a red X.
The proof I used is as follows:

Keybase => proof@ariadne.id=https://keybase.io/nicfab
Where am I wrong?

aspensmonster:

I'm not sure. At first glance, the public key fingerprint, 9a85d44ae2d38f19d5ffe2c8655812fca35b817f, is where it should be in the KeyOxide API response (['them', 'public_keys', 'primary', 'key_fingerprint']). The claim itself looks good in the public key. When I run the app locally, I sometimes get a timeout when attempting to fetch the proof from the Keybase API (but sometimes don't).

However, I think the main issue is that the proof from the API ultimately has just the fingerprint itself, 9a85d44ae2d38f19d5ffe2c8655812fca35b817f, while the proof checker is looking for the string openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f. This fails, and thus the proof check fails. I'm thinking that there might be a bug in whatever is setting params.target to openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f when calling containsProof. From what I can tell, the Claim itself has its _fingerprint property as openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f. But... all the other claims seem to have _fingerprint set that way and they verify fine. So perhaps the bug is elsewhere.

Indeed, the only serviceProviders that have target.format as E.ClaimFormat.FINGERPRINT rather than E.ClaimFormat.URI are Keybase, Lichess, and Owncast. That led me to looking at generateClaim inside of containsProof:

 function generateClaim (fingerprint, format) {
 switch (format) {
 case ClaimFormat.URI:
 if (fingerprint.match(/^(openpgp4fpr|aspe):/)) {
 return fingerprint
 }
 return `openpgp4fpr:${fingerprint}`
 case ClaimFormat.FINGERPRINT:
 return fingerprint
 default:
 throw new Error('No valid claim format')
 }
 }

Interestingly, this code has what looks like a defensive check for the wrong input fingerprint in the case Claim.Format.URI scenario, ensuring that openpgp4fpr:123456789 is returned regardless of whether fingerprint was set that way originally or not. But on the case Claim.Format.FINGERPRINT scenario, the fingerprint input is trusted as-is and returned. When I adjust the code to the following and run locally, the keybase claim verifies successfully:

export function generateClaim (fingerprint, format) {
 switch (format) {
 case ClaimFormat.URI:
 if (fingerprint.match(/^(openpgp4fpr|aspe):/)) {
 return fingerprint
 }
 return `openpgp4fpr:${fingerprint}`
 case ClaimFormat.FINGERPRINT:
 if (fingerprint.match(/^(openpgp4fpr|aspe):/)) {
 return fingerprint.split(':')[1]
 }
 return fingerprint
 default:
 throw new Error('No valid claim format')
 }
}

I'll file a bug report on the doipjs repo.

See the [Matrix chat](https://matrix.to/#/!dEfJkFpQpwvbzcegRX:matrix.org/$ESWc427xVJKw7jKJCKxo9vNodNofzzwdv69TROyDfWE?via=matrix.org&via=mackenba.ch&via=tchncs.de) for context, reproduced here: nicfab: >Hi, >My profile is https://keyoxide.org/9a85d44ae2d38f19d5ffe2c8655812fca35b817f, but - as you can see - on Keybase, a red X. The proof I used is as follows: > >Keybase => proof@ariadne.id=https://keybase.io/nicfab >Where am I wrong? aspensmonster: >I'm not sure. At first glance, the public key fingerprint, `9a85d44ae2d38f19d5ffe2c8655812fca35b817f`, is where it should be in the KeyOxide API response (`['them', 'public_keys', 'primary', 'key_fingerprint']`). The claim itself looks good in the public key. When I run the app locally, I sometimes get a timeout when attempting to fetch the proof from the Keybase API (but sometimes don't). > >However, I think the main issue is that the proof from the API ultimately has just the fingerprint itself, `9a85d44ae2d38f19d5ffe2c8655812fca35b817f`, while the proof checker is looking for the string `openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f`. This fails, and thus the proof check fails. I'm thinking that there might be a bug in whatever is setting `params.target` to `openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f` when calling `containsProof`. From what I can tell, the `Claim` itself has its `_fingerprint` property as `openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f`. But... all the other claims seem to have `_fingerprint` set that way and they verify fine. So perhaps the bug is elsewhere. > >Indeed, the only `serviceProvider`s that have `target.format` as `E.ClaimFormat.FINGERPRINT` rather than `E.ClaimFormat.URI` are Keybase, Lichess, and Owncast. That led me to looking at `generateClaim` inside of `containsProof`: > ``` function generateClaim (fingerprint, format) { switch (format) { case ClaimFormat.URI: if (fingerprint.match(/^(openpgp4fpr|aspe):/)) { return fingerprint } return `openpgp4fpr:${fingerprint}` case ClaimFormat.FINGERPRINT: return fingerprint default: throw new Error('No valid claim format') } } ``` > >Interestingly, this code has what looks like a defensive check for the wrong input `fingerprint` in the case `Claim.Format.URI` scenario, ensuring that `openpgp4fpr:123456789` is returned regardless of whether fingerprint was set that way originally or not. But on the case `Claim.Format.FINGERPRINT` scenario, the fingerprint input is trusted as-is and returned. When I adjust the code to the following and run locally, the keybase claim verifies successfully: > ``` export function generateClaim (fingerprint, format) { switch (format) { case ClaimFormat.URI: if (fingerprint.match(/^(openpgp4fpr|aspe):/)) { return fingerprint } return `openpgp4fpr:${fingerprint}` case ClaimFormat.FINGERPRINT: if (fingerprint.match(/^(openpgp4fpr|aspe):/)) { return fingerprint.split(':')[1] } return fingerprint default: throw new Error('No valid claim format') } } ``` > >I'll file a bug report on the doipjs repo.

I'm thinking that there might be a bug in whatever is setting params.target to openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f when calling containsProof. From what I can tell, the Claim itself has its _fingerprint property as openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f. But... all the other claims seem to have _fingerprint set that way and they verify fine. So perhaps the bug is elsewhere.

So I went on a journey through the codebase to figure out how the hell Claim#fingerprint ended up being URI when:

  1. The attached JSDoc example for Claim suggests using fingerprint with no mention of URIs:
    Lines 40 to 43 in edc3f40
    * @example
    * const claim = doip.Claim();
    * const claim = doip.Claim('dns:domain.tld?type=TXT');
    * const claim = doip.Claim('dns:domain.tld?type=TXT', '123abc123abc');
  2. And furthermore constructor includes an alphanumeric check that surely must fail on characters like : or /:
    Lines 51 to 59 in edc3f40
    // Verify validity of fingerprint
    if (fingerprint) {
    try {
    // @ts-ignore
    isAlphanumeric.default(fingerprint)
    } catch (err) {
    throw new Error('Invalid fingerprint')
    }
    }

I've opened Nic's profile in Keyoxide and saw that the server returned HTML with Keybase <kx-item /> whose data-claim was set to:

{
 "claimVersion": 2,
 "uri": "https://keybase.io/nicfab",
 "proofs": [
 "openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f"
 ],
 // stripped irrelevant parts
}

I went to check how the server populates <kx-item />-s and found out it just iterates first over Profile#personas and then over claims Persona#claims:

each claim in persona.claims
if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim,data-status='running')

The next question that popped inside my head was: Ok, but who creates the Profile instance? 🤔
A quick search for new Profile(...) invocations reveals it gets created in 4 possible ways:

  1. As a result of parsing fetched PGP key
  2. As a result of parsing fetched ASPE
  3. As a result of parsing fetched signature profile
  4. By deserializing back Profile's JSON representation

In this case, we are dealing with a PGP key so the code we are looking for is:

Lines 284 to 333 in edc3f40
export async function parsePublicKey (publicKey) {
if (!(publicKey && (publicKey instanceof PublicKey))) {
throw new Error('Invalid public key')
}
const fingerprint = publicKey.getFingerprint()
const primaryUser = await publicKey.getPrimaryUser()
const users = publicKey.users
const personas = []
users.forEach((user, i) => {
if (!user.userID) return
const pe = new Persona(user.userID.name, [])
pe.setIdentifier(user.userID.userID)
pe.setDescription(user.userID.comment)
pe.setEmailAddress(user.userID.email)
if ('selfCertifications' in user && user.selfCertifications.length > 0) {
const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0]
if (selfCertification.revoked) {
pe.revoke()
}
const notations = selfCertification.rawNotations
pe.claims = notations
.filter(
({ name, humanReadable }) =>
humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
)
.map(
({ value }) =>
new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
)
}
personas.push(pe)
})
const profile = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, personas)
profile.primaryPersonaIndex = primaryUser.index
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.fingerprint = fingerprint
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
return profile
}

Probably the most interesting part is this:

persona.claims = notations
 .filter(
 ({ name, humanReadable }) =>
 humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
 )
 .map(
 ({ value }) =>
 new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
 )

And that's how the claim got full URI instead of the just fingerprint. Still, it is beyond my understanding how this constructor call manages to run without runtime exceptions due to an alphanumeric check being run on the second argument. When serialized Claim will simply set the proofs key to an array with Claim#fingerprint value:

Lines 347 to 350 in edc3f40
return {
claimVersion: 2,
uri: this._uri,
proofs: [this._fingerprint],

matching a snippet I pasted earlier.

Now the frontend app kicks in and tries to deserialize the claim from <kx-item />'s data-claim attribute using:

Lines 409 to 422 in edc3f40
function importJsonClaimVersion2 (claimObject) {
if (!('claimVersion' in claimObject && claimObject.claimVersion === 2)) {
return new Error('Invalid claim')
}
const claim = new Claim()
claim._uri = claimObject.uri
claim._fingerprint = claimObject.proofs[0]
claim._matches = claimObject.matches.map(x => new ServiceProvider(x))
claim._status = claimObject.status
return claim
}

And there I found the code that killed me:

const claim = new Claim()
claim._uri = claimObject.uri
claim._fingerprint = claimObject.proofs[0]

You are certainly asking yourself why the hell it doesn't look like this:

const claim = new Claim(claimObject.uri, claimObject.proofs[0])

Well, the answer is, if it did and if the alphanumeric check on fingerprint actually worked it will simply blow up in your face saying that the URI you tried to smuggle in as fingerprint contains non-alphanumeric characters - namely :!

The next step is to stop for a minute and think what was the motivation behind this hackety-hack change. And the answer is quite simple actually. Before introducing ASPs only fingerprint Claim could receive was a PGP fingerprint. Now we have an ASP fingerprint too and because the Claim is key agnostic it needs to get a full URI because what generateClaim (located here:

Lines 59 to 71 in edc3f40
export function generateClaim (fingerprint, format) {
switch (format) {
case ClaimFormat.URI:
if (fingerprint.match(/^(openpgp4fpr|aspe):/)) {
return fingerprint
}
return `openpgp4fpr:${fingerprint}`
case ClaimFormat.FINGERPRINT:
return fingerprint
default:
throw new Error('No valid claim format')
}
}

) used to do is depending on passed format argument was either:

  1. expansion of fingerprint into full URI for ClaimFormat.URI (e.g. "9a85d44ae2d38f19d5ffe2c8655812fca35b817f" becomes "openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f"
  2. nothing 🙃 for ClaimFormat.FINGERPRINT cause you already have a fingerprint

In this new dual world of PGP and ASP fingerprints generateClaim has zero idea how to do 1. because it doesn't know which scheme (openpgp4fpr or aspe) to prepend! And finally to get rid of ambiguity fingerprint was simply switched through the whole codebase with full URI without changing any of the variable names, documentation snippets, and/or examples! Obviously, the hack didn't get executed flawlessly so now we need #64 to fix 2. also cause what generateClaim now does:

  1. nothing for ClaimFormat.URI because the fingerprint is now a URI
  2. should strip scheme from fingerprint(URI) when ClaimFormat.FINGERPRINT is requested (e.g. "openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f" becomes "9a85d44ae2d38f19d5ffe2c8655812fca35b817f")

Long story short, yeah #64 will make the original hack work (with @tyy's comment included!) but after applying that we should really go back and:

  1. change all fingerprint named arguments and properties to fingerprintUri
  2. stop using strings for passing URIs around - introduce the FingerprintURI class
  3. update documentation and examples!
> I'm thinking that there might be a bug in whatever is setting params.target to openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f when calling containsProof. From what I can tell, the Claim itself has its _fingerprint property as openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f. But... all the other claims seem to have _fingerprint set that way and they verify fine. So perhaps the bug is elsewhere. So I went on a journey through the codebase to figure out how the hell `Claim#fingerprint` ended up being URI when: 1. The attached JSDoc example for `Claim` suggests using fingerprint with no mention of URIs: https://codeberg.org/keyoxide/doipjs/src/commit/edc3f401bc88055339e59c7ad644a015f16c4c23/src/claim.js#L40-L43 2. And furthermore constructor includes an alphanumeric check that surely must fail on characters like `:` or `/`: https://codeberg.org/keyoxide/doipjs/src/commit/edc3f401bc88055339e59c7ad644a015f16c4c23/src/claim.js#L51-L59 I've opened [Nic's profile](https://keyoxide.org/hkp/9a85d44ae2d38f19d5ffe2c8655812fca35b817f) in Keyoxide and saw that the server returned HTML with Keybase `<kx-item />` whose `data-claim` was set to: ```json { "claimVersion": 2, "uri": "https://keybase.io/nicfab", "proofs": [ "openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f" ], // stripped irrelevant parts } ``` I went to check how the server populates `<kx-item />`-s and found out it just iterates first over `Profile#personas` and then over claims `Persona#claims`: https://codeberg.org/keyoxide/keyoxide-web/src/commit/9e19a622d9ceaa17875b664d1220c8c627d2e6f8/views/profile.pug#L18-L20 The next question that popped inside my head was: Ok, but who creates the `Profile` instance? 🤔 A quick search for `new Profile(...)` invocations reveals it gets created in 4 possible ways: 1. As a result of parsing fetched PGP key 2. As a result of parsing fetched ASPE 3. As a result of parsing fetched signature profile 4. By deserializing back `Profile`'s JSON representation In this case, we are dealing with a PGP key so the code we are looking for is: https://codeberg.org/keyoxide/doipjs/src/commit/edc3f401bc88055339e59c7ad644a015f16c4c23/src/openpgp.js#L284-L333 Probably the most interesting part is this: ```js persona.claims = notations .filter( ({ name, humanReadable }) => humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz') ) .map( ({ value }) => new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`) ) ``` And that's how the claim got full URI instead of the just fingerprint. Still, it is beyond my understanding how this constructor call manages to run without runtime exceptions due to an alphanumeric check being run on the second argument. When serialized `Claim` will simply set the `proofs` key to an array with `Claim#fingerprint` value: https://codeberg.org/keyoxide/doipjs/src/commit/edc3f401bc88055339e59c7ad644a015f16c4c23/src/claim.js#L347-L350 matching a snippet I pasted earlier. Now the frontend app kicks in and tries to deserialize the claim from `<kx-item />`'s `data-claim` attribute using: https://codeberg.org/keyoxide/doipjs/src/commit/edc3f401bc88055339e59c7ad644a015f16c4c23/src/claim.js#L409-L422 And there I found the code that _killed_ me: ```js const claim = new Claim() claim._uri = claimObject.uri claim._fingerprint = claimObject.proofs[0] ``` You are certainly asking yourself why the hell it doesn't look like this: ```js const claim = new Claim(claimObject.uri, claimObject.proofs[0]) ``` Well, the answer is, if it did and if the alphanumeric check on fingerprint actually worked it will simply blow up in your face saying that the URI you tried to smuggle in as fingerprint contains non-alphanumeric characters - namely `:`! The next step is to stop for a minute and think what was the motivation behind this hackety-hack change. And the answer is quite simple actually. Before introducing ASPs only fingerprint `Claim` could receive was a PGP fingerprint. Now we have an ASP fingerprint too and because the `Claim` is _key agnostic_ it needs to get a full URI because what `generateClaim` (located here: https://codeberg.org/keyoxide/doipjs/src/commit/edc3f401bc88055339e59c7ad644a015f16c4c23/src/utils.js#L59-L71) **used to do** is depending on passed format argument was either: 1. expansion of fingerprint into full URI for `ClaimFormat.URI` (e.g. "9a85d44ae2d38f19d5ffe2c8655812fca35b817f" becomes "openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f" 2. nothing 🙃 for `ClaimFormat.FINGERPRINT` cause you already have a fingerprint In this new dual world of PGP and ASP fingerprints `generateClaim` has zero idea how to do 1. because it doesn't know which scheme (openpgp4fpr or aspe) to prepend! And finally to get rid of ambiguity fingerprint was simply switched through the whole codebase with full URI without changing any of the variable names, documentation snippets, and/or examples! Obviously, the hack didn't get executed flawlessly so now we need #64 to fix 2. also cause what `generateClaim` **now does**: 1. nothing for `ClaimFormat.URI` because the fingerprint is now a URI 2. should strip scheme from fingerprint(URI) when `ClaimFormat.FINGERPRINT` is requested (e.g. "openpgp4fpr:9a85d44ae2d38f19d5ffe2c8655812fca35b817f" becomes "9a85d44ae2d38f19d5ffe2c8655812fca35b817f") Long story short, yeah #64 will make the original hack work (with @tyy's [comment](https://codeberg.org/keyoxide/doipjs/pulls/64#issuecomment-1539723) included!) but after applying that we should really go back and: 1. change all `fingerprint` named arguments and properties to `fingerprintUri` 2. stop using strings for passing URIs around - introduce the `FingerprintURI` class 3. update documentation and examples!

Ijust want to make a huge shoutout to @vladimyr and say thank you for your deep dive and very good explanation!

Ijust want to make a huge shoutout to @vladimyr and say *thank you* for your deep dive and very good explanation!

@McPringle You are welcome but the ones who actually deserve shoutouts are folks who worked tirelessly to get to the point where we have actual working software ready to deep dive into in search for bugs and ones that are soon going to fix those ;)

@McPringle You are welcome but the ones who actually deserve shoutouts are folks who worked tirelessly to get to the point where we have actual working software ready to deep dive into in search for bugs and ones that are soon going to fix those ;)

Do we know someone with a Keybase claim that I can use to check?

Do we know someone with a Keybase claim that I can use to check?

@Ryuno-Ki wrote in #63 (comment):

Do we know someone with a Keybase claim that I can use to check?

You can use my profile: https://keyoxide.org/wkd/gonzalob@gonz0.com.ar

& I'm available for whatever test or discussion or whatever is needed.

@Ryuno-Ki wrote in https://codeberg.org/keyoxide/doipjs/issues/63#issuecomment-5788616: > Do we know someone with a Keybase claim that I can use to check? You can use my profile: https://keyoxide.org/wkd/gonzalob@gonz0.com.ar & I'm available for whatever test or discussion or whatever is needed.

Here is my own Keyoxide profile with a Keybase claim for testing as well: https://keyoxide.org/ebkalderon@gmail.com

Still unable to verify this Keybase account at the time of writing, sadly.

Here is my own Keyoxide profile with a Keybase claim for testing as well: https://keyoxide.org/ebkalderon@gmail.com Still unable to verify this Keybase account at the time of writing, sadly.
Sign in to join this conversation.
No Branch/Tag specified
main
dev
fix-irc-fetcher
new-claim-eveonline-143
prepare-new-release
fix-jsdoc-types
yarn-to-npm
support-openpgp-aspe-claims
support-html-alias
improve-activitypub-support
v1-restructure
into-es-module
support-aspe
fix-js-lsp-issues
improve-linting
add-markers
support-opencollective-claim
support-entity-decoding
use-rome-tools
support-cloudflare-buster
support-fediverse-posts
matrix-room-verification
2.1.0
2.1.0-rc.3
2.1.0-rc.2
2.1.0-rc.1
2.0.1
2.0.0
2.0.0-rc.1
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.1.1
1.1.0
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
v0.19.1-alpha.0
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.0
0.13.0
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.5
0.8.4
0.8.3
0.8.1
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
Labels
Clear labels
bug
Something is not working

Archived

enhancement
New feature

Archived

Contribution welcome
Get started contributing here
Good first issue
Good if you are new to the project or to open source contributions
Impact
External
Affects the people using the project
Impact
Internal
Affects on the people working on the project
Priority
Critical
Work on right now
Priority
High
Work on at earliest convenience
Priority
Low
Work on in spare time
Priority
Medium
Work on regularly
Review
Duplicate
Already exists
Review
Off Topic
Does not fall within the scope of the repo/project
Status
Backlog
Is not being worked on yet
Status
Blocked
Is waiting on something or someone
Status
Completed
Is done
Status
In Progress
Is being worked on
Status
Investigating
Is waiting on research or questions
Status
Needs Decision
Is waiting on a decision by the devs/contributors
Status
Needs Info
Is waiting on additional information before it can be solved
Status
Needs Triage
Is new issue that needs reviewing
Status
Testing
Is being checked and verified
Status
Waiting For Review
Is waiting on reviewers to approve
Status
Won't Fix
Won't be fixed
Type
Bug
Related to something not working as intended
Type
CI
Related to continuous integration
Type
Documentation
Related to documentation
Type
Enhancement
Related to a new feature or an improved one
Type
New Claim
Related to a new identity claim/proof
Type
Security
Related to security
Type
Tests
Related to code tests
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
6 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
keyoxide/doipjs#63
Reference in a new issue
keyoxide/doipjs
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?