Go SDK for Solidus Network — did: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.
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.
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.
Found while porting, and they apply to every implementation, not just this one:
- Nonce endianness is unverified.
tx-create-v1.jsonusesnonce = 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. - Non-empty
serviceEndpointsis unverified. The vector uses[]. That hides Rust's[]vs Go'snil-marshals-to-null, and hides the fact that Go's JSON encoder escapes&whileserde_jsondoes 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.
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.
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.
go get github.com/solidusnetwork/solidus-goNo 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.
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])
}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[].publicKeyMultibase — z6Mk…, with the 0xed01 header |
active: true |
deactivated: false — negated |
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.
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 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.
Apache-2.0.