Revaulter
GitHub

Using Revaulter as a Go library

Everything revaulter-cli does over is also available as a Go package, so applications can submit encrypt, decrypt, sign, and verify requests without shelling out to the CLI.

go get github.com/italypaleale/revaulter/pkg/revaulter

The package documentation is on pkg.go.dev .

Only pkg/revaulter is part of the public API and offers a stable API contract.

Creating a client#

A client needs the address of a Revaulter server and a per-user request key, which is shown in the web UI after registration:

client, err := revaulter.New(revaulter.Options{
    Server:     "https://revaulter.example.com",
    RequestKey: os.Getenv("REVAULTER_REQUEST_KEY"),
})
if err != nil {
    return err
}

If the user authenticates requests with OIDC tokens , pass a JWT from one of their trusted issuers as OIDCToken instead of RequestKey, and set UserID to the user ID shown in the web UI:

client, err := revaulter.New(revaulter.Options{
    Server:    "https://revaulter.example.com",
    OIDCToken: oidcToken,
    UserID:    os.Getenv("REVAULTER_USER_ID"),
})

The client only uses the token to submit requests and fetch public keys: it retrieves each result with a per-request result token issued by the server, so an approval can take longer than the token’s lifetime.

OIDCToken holds a single token. For a long-lived Client, set OIDCTokenProvider instead, which the client calls it every time it needs the credential. The provided function should cache the token until it’s about to expire:

client, err := revaulter.New(revaulter.Options{
    Server: "https://revaulter.example.com",
    OIDCTokenProvider: func(ctx context.Context) string {
        // Return a cached token, requesting a new one when it's about to expire
        return tokenSource.Token(ctx)
    },
    UserID: os.Getenv("REVAULTER_USER_ID"),
})

Clients are safe for concurrent use. Every operation blocks until the user approves the request in the browser, the operation times out, or the context is canceled:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

Anchor pinning#

Each user owns a hybrid (ES384 + ML-DSA-87) anchor key pair that signs the public keys the server advertises. Clients pin the anchor on first contact (the same Trust On First Use model as SSH host keys) and refuse to proceed if it ever changes. Pins live in the trust store shared with revaulter-cli, so a server pinned with revaulter-cli trust is already trusted by the library.

Because pinning an anchor is a security decision, keys must explicitly pinned. Applications that can ask interactive user input should implement the callback:

client, err := revaulter.New(revaulter.Options{
    Server:     "https://revaulter.example.com",
    RequestKey: requestKey,
    ConfirmAnchor: func(anchor revaulter.AnchorInfo) (bool, error) {
        fmt.Printf("First contact with %s\nFingerprint:\n%s\n", anchor.Server, anchor.FormatFingerprint(2))
        // Ask the user, and return true to pin the anchor
        return promptUser()
    },
})

Non-interactive applications should either pin the anchor ahead of time with revaulter-cli trust, or opt into revaulter.AcceptAnchorOnFirstUse, which pins whatever the server presents on the first connection.

Encrypting and decrypting#

enc, err := client.Encrypt(ctx, revaulter.EncryptRequest{
    KeyLabel:  "my-secret",
    Algorithm: revaulter.AlgorithmA256GCM,
    Plaintext: []byte("hello world"),
    Note:      "backup credentials",
})
if err != nil {
    return err
}

// enc.Ciphertext, enc.Nonce, and enc.Tag are what you store
dec, err := client.Decrypt(ctx, revaulter.DecryptRequest{
    KeyLabel:   enc.KeyLabel,
    Algorithm:  enc.Algorithm,
    Ciphertext: enc.Ciphertext,
    Nonce:      enc.Nonce,
    Tag:        enc.Tag,
})
if err != nil {
    return err
}

fmt.Println(string(dec.Plaintext))

Payloads are limited to revaulter.MaxPayloadSize (100 KB), because they are sent to the browser to be processed. Larger data should be encrypted locally with a key that Revaulter wraps, which is what revaulter-edit does for files.

Signing and verifying#

Sign hashes the message as the algorithm requires before sending it, so for ES256 and Ed25519ph only the digest leaves the machine:

res, err := client.Sign(ctx, revaulter.SignRequest{
    KeyLabel:  "release",
    Algorithm: revaulter.AlgorithmES256,
    Message:   manifest,
})

res.Signature holds the raw 64-byte signature, in the IEEE P1363 r||s form for ES256.

Verifying does not require the user’s approval: it fetches the public signing key published on the server and checks the signature locally.

err = client.Verify(ctx, revaulter.VerifyRequest{
    KeyLabel:  "release",
    Algorithm: revaulter.AlgorithmES256,
    Message:   manifest,
    Signature: res.Signature,
})
if errors.Is(err, revaulter.ErrInvalidSignature) {
    // The signature does not match the message
}

Signing keys must be published from the Revaulter web interface: publishing attaches an anchor-signed proof, which the library verifies against the pinned anchor before returning the key. To verify many signatures, fetch the key once and reuse it:

key, err := client.SigningPublicKey(ctx, "release", revaulter.AlgorithmES256)
if err != nil {
    return err
}

err = key.Verify(revaulter.VerifyRequest{Message: manifest, Signature: signature})

key.PublicKey is a standard *ecdsa.PublicKey or ed25519.PublicKey, so it can also be handed to any other library.

Edit this page on GitHub