didcomm

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 25 Imported by: 0

README

go-didcomm

DIDComm v2 messaging for Go — signed, anonymous-encrypted, and authenticated-encrypted (ECDH-1PU) messages, with sender authentication that is on by default and bound to the message's declared sender.

Why this library

  • Private keys never enter the library. You implement a KeyStore (sign + Diffie-Hellman); the library only ever receives operation results, never a private key. Back it with an HSM/KMS and key material never leaves it.
  • Authenticated sender, returned as a value. Unpack verifies the sender and checks it against the message's from, returning the verified DID. Unsigned and sender-anonymous messages are rejected unless you explicitly opt out with UnpackUnverified.
  • Interoperable. Selectable per message: sign-then-encrypt (non-repudiable), ECDH-1PU authcrypt draft-03 and draft-04, and anoncrypt.
  • No decryption oracles. A self-contained JWE codec does constant-time MAC-then-decrypt and returns a single opaque error for every failure.

Install

go get github.com/notabene-id/go-didcomm

Quickstart

ctx := context.Background()

// Public DID documents plus private key material. In production the material
// goes into your KMS; softkey is an in-memory store for tests and local dev.
aliceDoc, aliceKeys, _ := didcomm.GenerateDIDKey()
bobDoc, bobKeys, _ := didcomm.GenerateDIDKey()

resolver, overrides := didcomm.DefaultResolver()
overrides.Store(aliceDoc)
overrides.Store(bobDoc)

store, _ := softkey.New(aliceKeys, bobKeys)
client := didcomm.NewClient(resolver, store)

msg := &didcomm.Message{
    ID: "1", Type: "https://example.com/hello",
    From: aliceDoc.ID, To: []string{bobDoc.ID},
    Body: json.RawMessage(`{"text":"Hello Bob!"}`),
}

packed, _ := client.Pack(ctx, msg, didcomm.WithProfile(didcomm.ProfileAuthcrypt1PUv3))

got, meta, err := client.Unpack(ctx, packed)
// err is non-nil for an unauthenticated, spoofed, or undecryptable message.
fmt.Println(meta.SenderDID) // == got.From, cryptographically verified

Profiles

Pick per message with WithProfile. The default is ProfileSignedAnoncrypt.

Profile Wire form Sender auth Repudiation
ProfileSignedAnoncrypt inner EdDSA JWS + ECDH-ES JWE inner signature non-repudiable
ProfileAuthcrypt1PUv3 ECDH-1PU JWE (draft-03) envelope (1PU) repudiable
ProfileAuthcrypt1PUv4 ECDH-1PU JWE (draft-04) envelope (1PU) repudiable
ProfileAnoncrypt ECDH-ES JWE none
ProfileSigned EdDSA JWS, no encryption signature non-repudiable

WithContentEncryption (A256GCM or A256CBC-HS512) and WithSerialization (general JSON or compact) refine the encrypted profiles; ProfileAuthcrypt1PUv4 fixes A256CBC-HS512 per the draft.

Unpacking

// Default: requires authentication; guarantees meta.SenderDID == msg.From.
// Rejects plain and anonymous messages. Use this wherever you trust `from`.
msg, meta, err := client.Unpack(ctx, envelope)

// Opt-out: decrypts where possible but does NOT authenticate the sender —
// msg.From is attacker-controlled. For diagnostics or anonymous intake only.
msg, meta, err := client.UnpackUnverified(ctx, envelope)

Interfaces you implement

// The only channel for private-key material. Keys never leave your implementation.
type KeyStore interface {
    Sign(ctx context.Context, kid string, data []byte) ([]byte, error)
    DiffieHellman(ctx context.Context, kid string, peerPublicKey []byte) ([]byte, error)
}

// DID resolution.
type DIDResolver interface {
    Resolve(ctx context.Context, did string) (*DIDDocument, error)
}

Built-in resolvers: DIDKeyResolver (local), DIDWebResolver (HTTPS), MultiResolver, and DefaultResolver(). did:web resolution enforces a timeout, caps the response size, refuses redirects, and blocks private, loopback, and cloud-metadata addresses at dial time (DIDWebResolver.AllowLoopback re-enables loopback for local dev). Verification methods are read from publicKeyJwk, publicKeyBase58, or publicKeyMultibase.

CLI

go install github.com/notabene-id/go-didcomm/cmd/didcomm@latest

didcomm generate --dir alice                          # did:key identity + keys.json
didcomm pack   --keys alice/keys.json --profile 1pu-v3 < msg.json
didcomm unpack --keys bob/keys.json   < packed.json

generate writes keys 0600 and prints only the public document unless --print-private is given. pack --send and send --to accept http(s) URLs only.

Security

Sender authentication is bound to from; unauthenticated intake is a named opt-out. The JWE codec is constant-time and returns one opaque decryption error. Report vulnerabilities to security@notabene.id.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrKeyNotFound is returned when a key cannot be found for a kid.
	ErrKeyNotFound = errors.New("didcomm: key not found")

	// ErrDIDNotFound is returned when a DID document cannot be resolved.
	ErrDIDNotFound = errors.New("didcomm: DID not found")

	// ErrInvalidMessage is returned when a message is malformed or missing
	// required fields.
	ErrInvalidMessage = errors.New("didcomm: invalid message")

	// ErrUnsupportedKeyType is returned for an unsupported key or curve.
	ErrUnsupportedKeyType = errors.New("didcomm: unsupported key type")

	// ErrUnsupportedProfile is returned when a profile cannot be packed or a wire
	// envelope uses an algorithm this library does not implement.
	ErrUnsupportedProfile = errors.New("didcomm: unsupported profile")

	// ErrNoRecipients is returned when encryption is requested with no recipients.
	ErrNoRecipients = errors.New("didcomm: no recipients")

	// ErrNoSender is returned when an operation requires a sender and none is set.
	ErrNoSender = errors.New("didcomm: no sender")

	// ErrNoServiceEndpoint is returned when a DID document exposes no
	// DIDCommMessaging service endpoint.
	ErrNoServiceEndpoint = errors.New("didcomm: no service endpoint")

	// ErrBlockedAddress is returned when did:web resolution is refused because the
	// target resolves to a non-public (loopback, private, link-local, or
	// metadata) address.
	ErrBlockedAddress = errors.New("didcomm: blocked non-public address")

	// ErrDecryptFailed is the single, opaque failure for any decryption or
	// signature-verification error. It never reveals which stage failed, so it
	// cannot be used as a decryption oracle.
	ErrDecryptFailed = errors.New("didcomm: decryption or verification failed")

	// ErrUnauthenticated is returned by Unpack for a message with no verifiable
	// sender (plain or anonymous encryption). Use UnpackUnverified to accept such
	// messages, understanding that the sender is not authenticated.
	ErrUnauthenticated = errors.New("didcomm: message has no authenticated sender")

	// ErrSenderMismatch is returned when the cryptographically verified sender
	// does not match the message's declared "from".
	ErrSenderMismatch = errors.New("didcomm: signer does not match message sender")

	// ErrRecipientMismatch is returned when this recipient is not listed in the
	// message's "to", which would indicate a forwarded or misdirected message.
	ErrRecipientMismatch = errors.New("didcomm: recipient not in message addressees")
)

Sentinel errors returned across the package boundary. Match with errors.Is.

Functions

func DefaultResolver

func DefaultResolver() (*MultiResolver, *InMemoryResolver)

DefaultResolver creates a MultiResolver pre-configured with did:key and did:web resolvers, and an InMemoryResolver as fallback for manually stored documents. Returns both the MultiResolver (for use with Client) and the InMemoryResolver (for Store calls).

func GenerateDIDKey

func GenerateDIDKey() (*DIDDocument, *KeyMaterial, error)

GenerateDIDKey generates a new did:key with an Ed25519 signing key and its derived X25519 key-agreement key. It returns the public DID document and the private KeyMaterial for the caller to load into a KeyStore.

func GenerateDIDWeb

func GenerateDIDWeb(domain, path string) (*DIDDocument, *KeyMaterial, error)

GenerateDIDWeb generates a did:web with an Ed25519 signing key and its derived X25519 key-agreement key. The caller hosts the returned document at the resolved URL and loads the KeyMaterial into a KeyStore.

func SafeHTTPClient

func SafeHTTPClient(allowLoopback bool) *http.Client

SafeHTTPClient builds an HTTP client hardened against SSRF: it blocks non-public destinations (loopback, private, link-local, and cloud-metadata addresses) at dial time, times out, and refuses redirects. It is used for did:web resolution and is exported so callers that POST to a resolved serviceEndpoint (e.g. delivering a packed message) can apply the same guard. Set allowLoopback for local development.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client packs and unpacks DIDComm v2 messages. It resolves DIDs through a Resolver and performs every private-key operation through a KeyStore, so it never holds key material itself.

func NewClient

func NewClient(resolver DIDResolver, keys KeyStore) *Client

NewClient creates a Client backed by the given resolver and key store.

func (*Client) Pack

func (c *Client) Pack(ctx context.Context, msg *Message, opts ...PackOption) ([]byte, error)

Pack serializes and protects a message according to the chosen profile (default ProfileSignedAnoncrypt). See the Profile constants for the security properties of each.

func (*Client) Unpack

func (c *Client) Unpack(ctx context.Context, envelope []byte) (*Message, *Metadata, error)

Unpack decrypts and/or verifies an envelope and REQUIRES an authenticated sender. On success Metadata.SenderDID is cryptographically verified and equals Message.From. Plain and anonymously-encrypted messages are rejected with ErrUnauthenticated; use UnpackUnverified to accept them.

func (*Client) UnpackUnverified

func (c *Client) UnpackUnverified(ctx context.Context, envelope []byte) (*Message, *Metadata, error)

UnpackUnverified decrypts where it can but does NOT require an authenticated sender. For a returned message with an empty Metadata.SenderDID, Message.From is attacker-controlled and must not be trusted. Use it only for diagnostics, relaying, or anonymous intake.

type ContentEncryption

type ContentEncryption uint8

ContentEncryption identifies a content-encryption algorithm for WithContentEncryption.

const (
	ContentA256GCM ContentEncryption = iota
	ContentA256CBCHS512
)

Content-encryption choices.

type DIDDocument

type DIDDocument struct {
	ID                 string               `json:"id"`
	VerificationMethod []VerificationMethod `json:"verificationMethod,omitempty"`
	Authentication     []VerificationMethod `json:"authentication,omitempty"`
	KeyAgreement       []VerificationMethod `json:"keyAgreement,omitempty"`
	Service            []Service            `json:"service,omitempty"`
}

DIDDocument is a thin DID document with only DIDComm-relevant fields.

func (*DIDDocument) FindDIDCommEndpoint

func (doc *DIDDocument) FindDIDCommEndpoint() (string, error)

FindDIDCommEndpoint returns the first DIDCommMessaging service endpoint URL from the document.

func (*DIDDocument) FindEncryptionKey

func (doc *DIDDocument) FindEncryptionKey() (*VerificationMethod, error)

FindEncryptionKey returns the first X25519 key agreement key from a DID document, skipping keys of other curves (e.g. a P-256 PII key).

func (*DIDDocument) FindSigningKey

func (doc *DIDDocument) FindSigningKey() (*VerificationMethod, error)

FindSigningKey returns the first authentication key from a DID document.

func (*DIDDocument) UnmarshalJSON

func (doc *DIDDocument) UnmarshalJSON(data []byte) error

UnmarshalJSON handles DID document fields where authentication and keyAgreement can contain either inline verification method objects or string references to entries in the verificationMethod array.

type DIDKeyResolver

type DIDKeyResolver struct{}

DIDKeyResolver resolves did:key DIDs locally by decoding the multicodec-encoded public key.

func (*DIDKeyResolver) Resolve

func (r *DIDKeyResolver) Resolve(_ context.Context, did string) (*DIDDocument, error)

Resolve parses a did:key DID and returns a DIDDocument with authentication and key agreement keys.

type DIDResolver

type DIDResolver interface {
	Resolve(ctx context.Context, did string) (*DIDDocument, error)
}

DIDResolver resolves DIDs to DID documents.

type DIDWebResolver

type DIDWebResolver struct {
	// HTTPClient overrides the default guarded client. Setting it opts out of the
	// built-in SSRF protections, so supply your own guard if you do.
	HTTPClient *http.Client
	// AllowLoopback permits connections to loopback addresses, for local
	// development against a node on 127.0.0.1.
	AllowLoopback bool
}

DIDWebResolver resolves did:web DIDs over HTTPS. Because a did:web identifier in an inbound message is attacker-controlled, the default transport refuses to connect to loopback, private, link-local, or cloud-metadata addresses (checked at dial time, which also defeats DNS rebinding), sets a timeout, caps the response size, and does not follow redirects.

func (*DIDWebResolver) Resolve

func (r *DIDWebResolver) Resolve(ctx context.Context, did string) (*DIDDocument, error)

Resolve fetches and parses a did:web DID document.

type InMemoryResolver

type InMemoryResolver struct {
	// contains filtered or unexported fields
}

InMemoryResolver is a simple in-memory implementation of DIDResolver.

func NewInMemoryResolver

func NewInMemoryResolver() *InMemoryResolver

NewInMemoryResolver creates a new in-memory DID resolver.

func (*InMemoryResolver) Resolve

func (r *InMemoryResolver) Resolve(_ context.Context, did string) (*DIDDocument, error)

Resolve looks up a DID document by DID.

func (*InMemoryResolver) Store

func (r *InMemoryResolver) Store(doc *DIDDocument)

Store registers a DID document with the resolver.

type KeyMaterial

type KeyMaterial struct {
	DID             string `json:"did"`
	SigningKID      string `json:"signingKid"`      // Ed25519 signing verification-method id
	KeyAgreementKID string `json:"keyAgreementKid"` // X25519 key-agreement verification-method id
	Ed25519Seed     []byte `json:"ed25519Seed"`     // 32-byte Ed25519 private seed
	X25519Private   []byte `json:"x25519Private"`   // 32-byte X25519 private scalar
}

KeyMaterial is the private key material for a generated DID. It is returned to the caller, who loads it into a KeyStore (see the softkey package for a development store, or a KMS/HSM adapter in production). It is never passed back into the library — the library operates only through the sealed KeyStore.

type KeyStore

type KeyStore interface {
	// Sign returns the raw EdDSA signature over data for signing key kid.
	Sign(ctx context.Context, kid string, data []byte) ([]byte, error)

	// DiffieHellman returns the X25519 shared secret between key kid and
	// peerPublicKey. The long-term private key is not recoverable from it.
	DiffieHellman(ctx context.Context, kid string, peerPublicKey []byte) ([]byte, error)
}

KeyStore is the sole channel for private-key material: implementations hold the keys and return only operation results, so a compromised build of this library cannot exfiltrate a key. Back it with an HSM or KMS. Keys are named by their DID verification-method id (kid, e.g. "did:web:example.com#key-1").

type Message

type Message struct {
	ID        string          `json:"id"`
	Type      string          `json:"type"`
	From      string          `json:"from,omitempty"`
	To        []string        `json:"to,omitempty"`
	CreatedAt *time.Time      `json:"created_time,omitempty"`
	ExpiresAt *time.Time      `json:"expires_time,omitempty"`
	Body      json.RawMessage `json:"body"`
	Thid      string          `json:"thid,omitempty"`
	Pthid     string          `json:"pthid,omitempty"`
	Extra     map[string]any  `json:"-"`
}

Message represents a DIDComm v2 message.

func (*Message) MarshalJSON

func (m *Message) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON marshaling that includes Extra fields.

func (*Message) UnmarshalJSON

func (m *Message) UnmarshalJSON(data []byte) error

UnmarshalJSON implements custom JSON unmarshaling that captures extra fields.

func (*Message) Validate

func (m *Message) Validate() error

Validate checks that the message has required fields.

type Metadata

type Metadata struct {
	// Mode is the wire protection that was applied.
	Mode Mode
	// SenderDID is the cryptographically verified sender. For messages returned
	// by Unpack it always equals Message.From. It is empty only for anonymous or
	// plain messages, which Unpack rejects and only UnpackUnverified returns.
	SenderDID string
	// Encrypted reports whether the message was encrypted on the wire.
	Encrypted bool
	// Profile is the encrypted profile that was detected, when Encrypted.
	Profile Profile
}

Metadata describes how an unpacked message was protected.

type Mode

type Mode uint8

Mode reports how an unpacked message was protected on the wire.

const (
	ModeSigned Mode = iota
	ModeAuthcrypt
	ModeAnoncrypt
	ModePlain
)

Unpacked message modes.

type MultiResolver

type MultiResolver struct {
	// contains filtered or unexported fields
}

MultiResolver routes DID resolution to method-specific resolvers based on the DID prefix.

func NewMultiResolver

func NewMultiResolver(methods map[string]DIDResolver, fallback DIDResolver) *MultiResolver

NewMultiResolver creates a MultiResolver with the given method resolvers and optional fallback. The methods map keys should be DID method prefixes like "did:key" or "did:web".

func (*MultiResolver) Resolve

func (r *MultiResolver) Resolve(ctx context.Context, did string) (*DIDDocument, error)

Resolve routes the DID to the appropriate method-specific resolver.

type PackOption

type PackOption func(*packConfig)

PackOption configures a Pack call.

func WithContentEncryption

func WithContentEncryption(enc ContentEncryption) PackOption

WithContentEncryption overrides the JWE content-encryption algorithm. Ignored where a profile fixes it (ProfileAuthcrypt1PUv4 requires A256CBC-HS512).

func WithProfile

func WithProfile(p Profile) PackOption

WithProfile selects the envelope profile (default ProfileSignedAnoncrypt).

func WithSerialization

func WithSerialization(s Serialization) PackOption

WithSerialization selects the JWE serialization (default GeneralJSON). Compact supports a single recipient only.

type Profile

type Profile uint8

Profile selects the DIDComm envelope a message is packed into. The choice is a security and interoperability decision; see each constant.

const (
	// ProfileSignedAnoncrypt is sign-then-encrypt: an inner EdDSA JWS wrapped in
	// an ECDH-ES anonymous-sender JWE. Authentication is the inner signature, so
	// it is NON-repudiable (provable to third parties) — the recommended default
	// where a message's origin must stand up to later audit. RFC 7515 (JWS) +
	// RFC 7518 §4.6 (ECDH-ES).
	ProfileSignedAnoncrypt Profile = iota

	// ProfileAuthcrypt1PUv3 is authenticated encryption via ECDH-1PU key
	// agreement, draft-madden-jose-ecdh-1pu-03: the sender's static key enters
	// the KDF, authenticating the sender at the envelope with no inner signature.
	// Repudiable. Interoperates with implementations that predate the draft-04
	// tag binding.
	ProfileAuthcrypt1PUv3

	// ProfileAuthcrypt1PUv4 is ECDH-1PU authcrypt per draft-madden-jose-ecdh-1pu-04,
	// which folds the content-encryption tag into the key derivation (§2.3) and
	// requires the AES-CBC-HMAC content-encryption family.
	ProfileAuthcrypt1PUv4

	// ProfileAnoncrypt is anonymous encryption (ECDH-ES) with no sender
	// authentication. A message packed this way carries no verifiable sender and
	// is only readable through Client.UnpackUnverified.
	ProfileAnoncrypt

	// ProfileSigned is a signed-only JWS with no encryption. The body is
	// authenticated and non-repudiable but travels in the clear; use it only for
	// content that is not confidential (e.g. out-of-band invitations).
	ProfileSigned
)

type Serialization

type Serialization uint8

Serialization selects the JWE serialization for encrypted profiles.

const (
	GeneralJSON Serialization = iota
	Compact
)

JWE serialization forms.

type Service

type Service struct {
	ID              string `json:"id"`
	Type            string `json:"type"`
	ServiceEndpoint string `json:"serviceEndpoint"`
}

Service represents a DID document service entry.

type VerificationMethod

type VerificationMethod struct {
	ID         string  `json:"id"`
	Type       string  `json:"type"`
	Controller string  `json:"controller"`
	PublicKey  jwk.Key `json:"-"`
}

VerificationMethod represents a DID document verification method.

func (VerificationMethod) MarshalJSON

func (vm VerificationMethod) MarshalJSON() ([]byte, error)

MarshalJSON serializes a VerificationMethod including publicKeyJwk.

func (*VerificationMethod) UnmarshalJSON

func (vm *VerificationMethod) UnmarshalJSON(data []byte) error

UnmarshalJSON deserializes a VerificationMethod restoring publicKeyJwk into PublicKey.

Directories

Path Synopsis
cmd
didcomm command
Command didcomm generates DID identities and packs, unpacks, and sends DIDComm v2 messages from the shell.
Command didcomm generates DID identities and packs, unpacks, and sends DIDComm v2 messages from the shell.
internal
convert
Package convert provides Ed25519 to X25519 key conversion utilities.
Package convert provides Ed25519 to X25519 key conversion utilities.
jose
Package jose implements the JOSE primitives DIDComm v2 needs that are not available from the standard library or the jwx JWS layer: NIST SP 800-56A Concat KDF, RFC 3394 AES key wrap, RFC 7518 content encryption, and the ECDH-ES / ECDH-1PU key-agreement key derivation used to build JWE envelopes.
Package jose implements the JOSE primitives DIDComm v2 needs that are not available from the standard library or the jwx JWS layer: NIST SP 800-56A Concat KDF, RFC 3394 AES key wrap, RFC 7518 content encryption, and the ECDH-ES / ECDH-1PU key-agreement key derivation used to build JWE envelopes.
Package softkey provides an in-memory KeyStore for tests and local development.
Package softkey provides an in-memory KeyStore for tests and local development.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL