Skip to content

Repository files navigation

solidus-go

Go SDK for Solidus Networkdid:solidus derivation, identifier validation, the W3C verification-method encoding, strict Ed25519 verification, and DidCreate transaction signing.

Status: released at v0.1.0, 2026-09-01.

go get github.com/solidusnetwork/solidus-go@v0.1.0

Verified from a clean module by an outsider: go get, then a program importing it compiled and ran.

Read this before you evaluate it: BBS+ is missing, and it cannot be added

Solidus signs credentials with the ciphersuite from draft-irtf-cfrg-bbs-signatures. There is no Go implementation of it. Not "none that is mature" — none.

CFRG draft BBS (ours)   signature = A (G1,48) + e (Fr,32)             =  80 bytes
2020 BBS+ (every Go lib) signature = A (G1,48) + e (Fr,32) + s (Fr,32) = 112 bytes

The 2020 scheme carries a blinding scalar the draft removed, so a library from that lineage cannot parse a Solidus signature, let alone verify one. Checked against trustbloc/bbs-signature-go v1.0.4 and suutaku/go-bbs v0.0.5 — each verifies a signature it produced itself and rejects ours at parse time — and cloudflare/circl, which has BLS12-381 arithmetic and BLS signatures but no BBS at all. Each probe carried a control: the library verifying a signature it produced itself, so a failure on ours is a parse difference and not a broken test. The versions above are pinned precisely so anyone can reproduce it.

So this package cannot verify a Solidus credential. It covers derivation, DIDs, and transaction signing, which is enough for a gateway that authenticates DIDs and submits transactions, and not enough for one that checks credentials. Choose accordingly.

Conformance

test-vectors/ is published so third parties can check us rather than take our word. This package runs it and prints its own coverage:

5/12 vectors CHECKED. A green run does NOT mean 12/12.
3 out of scope, 4 unavailable to any Go implementation.

Three outcomes, not two. OUT OF SCOPE is a decision — the three credential-bundle vectors exercise a product-layer schema this package does not own. UNAVAILABLE is a constraint — the four BBS+ vectors cannot be checked by any Go code that exists. Conflating them would hide which one a commit could fix.

A category with no handler is a failure, never a skip.

⚠ Two things the conformance suite does not cover

Found while porting, and they apply to every implementation, not just this one:

  1. Nonce endianness is unverified. tx-create-v1.json uses nonce = 0, where the little-endian and big-endian encodings are byte-identical. An implementation that got it backwards passes the vector and produces invalid signatures for every transaction a real user sends.
  2. Non-empty serviceEndpoints is unverified. The vector uses []. That hides Rust's [] vs Go's nil-marshals-to-null, and hides the fact that Go's JSON encoder escapes & while serde_json does not — which would break every service endpoint carrying a query string. The Rust runner is explicit about the gap: "this runner only handles the empty serviceEndpoints case the vector uses".

tx_test.go pins both against the chain's definition, since no agreed signature exists for either. Those are change-detectors, not interop proofs. A vector with a non-zero nonce and at least one service endpoint would close the gap properly.

Ed25519 is strict

Verification rejects small-order public keys and non-canonical encodings — strongly binding signatures, i.e. exclusive ownership, the property a verifiable credential exists to assert.

crypto/ed25519 is cofactorless and enforces S < L, so it is closer to strict than most defaults. It does not reject small-order keys. ⚠ The all-zeros key is accepted for roughly one message in four — 959 of 4000 sampled — because that encoding is a point of order 4, not the identity, so the verification equation closes exactly when k = SHA-512(R‖A‖M) is divisible by 4. That makes the attack cheaper, not harder: grind four messages and one verifies under a key nobody holds. VerifyStrict adds the missing check via filippo.io/edwards25519 — multiply by the cofactor, compare against the identity point — and ships with the control that a real signature still verifies, because a verifier that rejects everything passes the strictness test and is useless.

Errors, never panics

Every exported function that parses untrusted input returns an error. The audience for a Go identity SDK is gateways and validators, and a gateway that panics on a malformed DID from an anonymous request is a denial-of-service.

Install

go get github.com/solidusnetwork/solidus-go

No registry account and no credential: a Go module is a public repository plus a tag.

Requires Go 1.25 or later, which is the floor golang.org/x/crypto and golang.org/x/text impose; nothing in this package needs it. go.mod previously declared go 1.26.5, a patch release, which refused anyone on 1.26.4 for no reason at all.

Usage

package main

import (
	"context"
	"fmt"

	solidus "github.com/solidusnetwork/solidus-go"
)

func main() {
	mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon " +
		"abandon abandon abandon abandon abandon abandon abandon abandon " +
		"abandon abandon abandon abandon abandon abandon abandon art"

	seed := solidus.SeedFromMnemonic(mnemonic, "")
	key := solidus.IdentityKey(seed)
	fmt.Println(key.DID(solidus.Testnet)) // did:solidus:testnet:3tBoVe6XRtirzr8SdRotGgbkuEQN

	// A distinct, unlinkable key per verifier.
	rp := solidus.PairwiseKey(seed, "rp-a.example.com")
	fmt.Println(rp.DID(solidus.Testnet))

	// Resolve, over JSON-RPC. Read-only: the resolver holds no key material.
	res, err := solidus.NewResolver("https://rpc.solidus.network").
		Resolve(context.Background(), key.DID(solidus.Testnet))
	if err != nil {
		// A transport or protocol failure — the node was unreachable, or did
		// not speak JSON-RPC. A DID that simply does not exist is NOT an error.
		panic(err)
	}
	switch res.DIDResolutionMetadata.Error {
	case "":
		// Consume the document. publicKeyMultibase, never publicKeyHex.
		vm := res.DIDDocument.VerificationMethod[0]
		pub, err := solidus.DecodePublicKeyMultibase(vm.PublicKeyMultibase)
		if err != nil {
			panic(err)
		}
		message, signature := []byte("a challenge"), make([]byte, 64)
		fmt.Println("verified:", solidus.VerifyStrict(pub, message, signature))
	case "notFound":
		fmt.Println("no such DID — but the identifier was well-formed")
	case "invalidDid":
		fmt.Println("malformed identifier; this never reached the network")
	}

	// Sign a DidCreate. nil serviceEndpoints is treated as an empty list, which
	// is what the chain expects — see tx.go.
	tx, err := solidus.SignDIDCreate(key.PrivateKey.Seed(), 0, nil)
	if err != nil {
		panic(err)
	}
	fmt.Printf("signature: %x\n", tx.Signature[:8])
}

The wire is not the W3C shape

solidus_didResolve returns the chain's document, and it does not look like what W3C tooling expects. Resolve performs the transformation; a client generated from @solidus-network/types would decode none of it, because those types describe the destination rather than the source.

chain wire W3C
context — a string @context — an array of two
verification_method[].publicKeyHex — raw hex verificationMethod[].publicKeyMultibasez6Mk…, with the 0xed01 header
active: true deactivated: falsenegated
created_ms — epoch milliseconds created — ISO 8601, UTC, three fractional digits
assertion_method absent (legacy records) mirrored from authentication

keyAgreement is never mirrored. A key that authenticates is not thereby a key-agreement key, and emitting one would assert something we did not check.

Development

The conformance vectors are a submodule, so clone with them:

git clone --recurse-submodules https://github.com/solidusnetwork/solidus-go.git
cd solidus-go
go build ./... && go vet ./... && go test ./...

If you already cloned without them, git submodule update --init is enough. Three tests read the vectors and fail with instructions if they are absent. They are a submodule rather than a copy so that the bytes cannot drift from the ones every other Solidus SDK is tested against.

go.mod carries no replace directive, deliberately. A local replace resolves on the author's machine and fails for everyone else — the same shape as the workspace:* dependency an outside developer filed against our npm packages.

The rest of the ecosystem

The same conformance vectors back three SDKs, and all three produce byte-identical output for the operations they share.

language package BBS+
Go github.com/solidusnetwork/solidus-go no, see above
Python solidus-network on PyPI yes
TypeScript @solidus-network/sdk on npm yes

The vectors themselves are at solidusnetwork/solidus-test-vectors, so a third implementation can check itself against the same bytes.

Licence

Apache-2.0.

About

Go SDK for the Solidus Network: did:solidus derivation, resolution, and strict Ed25519 verification.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages