Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5801377
feat: ed25519 wrapped seed interface
joe-p Feb 23, 2026
7d169cb
chore: fix npm audit
joe-p Feb 24, 2026
a9a93c4
chore: PR review feedback
joe-p Feb 24, 2026
f6ee4df
test: await runTests
joe-p Feb 24, 2026
f54df37
fix: ensure seed is properly zeroed out
joe-p Feb 24, 2026
8fe996b
feat: wrapped secret
joe-p Feb 27, 2026
03ce1d6
chore: update .nsprc
joe-p Feb 27, 2026
0f86212
chore: PR review
joe-p Feb 27, 2026
6e1742e
chore: remove 64-byte secret wrapping/unwrapping
joe-p Mar 2, 2026
39d11b6
chore: add signing examples
joe-p Mar 2, 2026
4ae99ee
chore: add mock KMSClient for CI
joe-p Mar 2, 2026
7a1fe28
docs: add secret-management.md
joe-p Mar 2, 2026
be9c99f
docs: AWS comment
joe-p Mar 2, 2026
52518f0
docs: add imports
joe-p Mar 2, 2026
78bf167
Merge branch 'decoupling' into decoupling-feat/wrapped_seed
joe-p Mar 2, 2026
554259f
Merge branch 'decoupling-feat/wrapped_seed' into decoupling-docs/wrap…
joe-p Mar 2, 2026
ef16f71
docs: add header comments
joe-p Mar 6, 2026
0591529
chore: use utils/crypto
joe-p Mar 12, 2026
56b6473
Merge branch 'decoupling' into decoupling-docs/wrapped_docs
joe-p Mar 13, 2026
464b329
Apply suggestions from code review
joe-p Mar 13, 2026
5d39e34
chore: rm FIXME from docs/examples
joe-p Mar 13, 2026
b2257a1
Potential fix for pull request finding
joe-p Mar 13, 2026
9b950fe
docs: fix grammar
joe-p Mar 13, 2026
208134e
Merge branch 'decoupling' into decoupling-docs/wrapped_docs
joe-p Mar 13, 2026
77ac1ac
chore: use sidebar config json
joe-p Mar 13, 2026
b10bd2e
chore: update sidebar config, examples loader and verification scripts
mrcointreau Mar 13, 2026
b8ab11c
chore: add nsprc exception for flatted vuln (1114526)
mrcointreau Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nsprc
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@
"active": true,
"notes": "Dev tooling dependency (npm -> tar)",
"expiry": "2026-03-29"
},
"1114526": {
"active": true,
"notes": "Dev tooling dependency (eslint -> flatted)",
"expiry": "2026-03-29"
}
}
4 changes: 3 additions & 1 deletion docs/sidebar.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
{ "slug": "concepts/core/account" },
{ "slug": "concepts/core/transaction" },
{ "slug": "concepts/core/amount" },
{ "slug": "concepts/core/client" }
{ "slug": "concepts/core/client" },
{ "slug": "concepts/core/secret-management" }
]
},
{
Expand Down Expand Up @@ -55,6 +56,7 @@
{ "label": "Common", "slug": "examples/common" },
{ "label": "Indexer Client", "slug": "examples/indexer-client" },
{ "label": "KMD Client", "slug": "examples/kmd-client" },
{ "label": "Signing", "slug": "examples/signing" },
{ "label": "Testing", "slug": "examples/testing" },
{ "label": "Transact", "slug": "examples/transact" }
]
Expand Down
228 changes: 228 additions & 0 deletions docs/src/content/docs/concepts/core/secret-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
---
title: Secret management
description: AlgoKit utils provides interfaces and concrete functions to enable secure management of secret material for signing transactions. This includes support for using an external KMS or key wrapping and unwrapping with a secrets manager.
---

In general, there are three levels of security when it comes to signing transactions with secret material:

1. KMS - The secret material is never exposed to the application
1. Key Wrapping and Unwrapping - The secret material is stored outside of the app (i.e. keychain) and only loaded in memory when signing
1. Plaintext - The secret material is stored in plaintext (i.e. in the environment) and is accessible throughout the runtime of the application

While using plaintext environment variables may be the easier to setup, it is **not recommended** for production use. A compromised environment and/or dependency could lead to the secret material being compromised. Additionally, it is easy to accidentally leak secrets in plaintext through git commits.

The most secure option is to use an external KMS that completely isolates the secret material from the application. KMS', however, can have a high setup cost which may be difficult for a solo developer or small team to manage properly. In this case, the next recommended option is to use key wrapping and unwrapping with a secrets manager. This allows the secret material to be stored securely outside of the application and only loaded in memory when signing is necessary. For example, on a local machine, the OS keyring can be used to store the secret material and only load it when signing transactions.

## Signing with a Wrapped Secret

### Using Keyring Secrets

To read a mnemonic from the OS keyring, you can use the `@napi-rs/keyring` library. This prevents the mnemonic from being stored in
plaintext and ensures it is only loaded in memory when signing.

#### Ed25519 Seed or Mnemonic

When working with a ed25519 seed or mnemonic, you can implement the `WrappedEd25519Seed` interface which allows you to wrap and unwrap the seed as needed. For example, with `@napi-rs/keyring`:

```ts
import { ed25519SigningKeyFromWrappedSecret, WrappedEd25519Seed } from '@algorandfoundation/algokit-utils/crypto'
import { algo, AlgorandClient, microAlgo } from '@algorandfoundation/algokit-utils'
import { mnemonicFromSeed, seedFromMnemonic } from '@algorandfoundation/algokit-utils/algo25'
import { generateAddressWithSigners } from '@algorandfoundation/algokit-utils/transact'
import { Entry } from '@napi-rs/keyring'

const wrappedSeed: WrappedEd25519Seed = {
unwrapEd25519Seed: async () => {
const entry = new Entry('algorand', MNEMONIC_NAME)
const mn = entry.getPassword()

if (!mn) {
throw new Error(`No mnemonic found in keyring for ${MNEMONIC_NAME}`)
}

return seedFromMnemonic(mn)
},
wrapEd25519Seed: async () => {},
}

const signingKey = await ed25519SigningKeyFromWrappedSecret(wrappedSeed)
const algorandAccount = generateAddressWithSigners(signingKey)

const algorand = AlgorandClient.defaultLocalNet()

await algorand.account.ensureFundedFromEnvironment(algorandAccount.addr, algo(1))

const pay = await AlgorandClient.defaultLocalNet().send.payment({
sender: algorandAccount,
receiver: algorandAccount,
amount: microAlgo(0),
})
```

### HD Expanded Secret Key

HD accounts have a 96-byte expanded secret key that can be used in a similar manner to the ed25519 seed, except we need to implement the `WrappedHdExtendedPrivateKey` interface. For example, with `@napi-rs/keyring`:

```ts
import {
ed25519SigningKeyFromWrappedSecret,
peikertXHdWalletGenerator,
WrappedHdExtendedPrivateKey,
} from '@algorandfoundation/algokit-utils/crypto'
import { algo, AlgorandClient, microAlgo } from '@algorandfoundation/algokit-utils'
import { generateAddressWithSigners } from '@algorandfoundation/algokit-utils/transact'
import { Entry } from '@napi-rs/keyring'

const wrappedSeed: WrappedHdExtendedPrivateKey = {
unwrapHdExtendedPrivateKey: async () => {
const entry = new Entry('algorand', SECRET_NAME)
const esk = entry.getSecret()

if (!esk) {
throw new Error(`No mnemonic found in keyring for ${SECRET_NAME}`)
}

// The last 32 bytes of the extended private key is the chain code, which is not needed for signing. This means in most cases you can
// just store the first 64 bytes and then pad the secret to 96 bytes in the unwrap function. If you are storing the full 96 bytes,
// you can just return the secret as is.
if (esk.length === 64) {
const paddedEsk = new Uint8Array(96)
paddedEsk.set(esk, 0)
return paddedEsk
}

return new Uint8Array(esk)
},
wrapHdExtendedPrivateKey: async () => {},
}

const signingKey = await ed25519SigningKeyFromWrappedSecret(wrappedSeed)
const algorandAccount = generateAddressWithSigners(signingKey)

const algorand = AlgorandClient.defaultLocalNet()

await algorand.account.ensureFundedFromEnvironment(algorandAccount.addr, algo(1))

await AlgorandClient.defaultLocalNet().send.payment({
sender: algorandAccount,
receiver: algorandAccount,
amount: microAlgo(0),
})
```

## Signing with a KMS

### Note on KMS Authentication in CI

If you are using a KMS in CI, the best practice for performing signing operations is to use OIDC. For guides for setting up OIDC, refer to the [GitHub documentation](https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments).

### Signing with AWS KMS

Using the KMS, you can retrieve the public key and implement `RawEd25519Signer` signer which can then be used to generate an Algorand address and all Algorand-specific signing functions. For example, with AWS:

```ts
import { RawEd25519Signer } from '@algorandfoundation/algokit-utils/crypto'
import { AlgorandClient, microAlgos } from '@algorandfoundation/algokit-utils'
import { generateAddressWithSigners } from '@algorandfoundation/algokit-utils/transact'
import { KMSClient, SignCommand, GetPublicKeyCommand, SignCommandInput, GetPublicKeyCommandInput } from '@aws-sdk/client-kms'

// The following environment variables must be set for this to work:
// - AWS_REGION
// - KEY_ID
// - AWS_ACCESS_KEY_ID
// - AWS_SECRET_ACCESS_KEY
const kms = new KMSClient({ region: process.env.AWS_REGION });

const rawEd25519Signer: RawEd25519Signer = async (data: Uint8Array): Promise<Uint8Array> => {
const resp = await kms.send(
new SignCommand({
KeyId: process.env.KEY_ID,
Message: data,
MessageType: "RAW",
SigningAlgorithm: "ED25519_SHA_512",
})
);

if (!resp.Signature) {
throw new Error("No signature returned from KMS");
}

return resp.Signature;
}

const pubkeyResp = await kms.send(new GetPublicKeyCommand({
KeyId: process.env.KEY_ID,
}));

if (!pubkeyResp.PublicKey) {
throw new Error("No public key returned from KMS");
}

const spki = Buffer.from(pubkeyResp.PublicKey as Uint8Array);


const ed25519SpkiPrefix = Buffer.from([
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00
]);

if (!spki.subarray(0, 12).equals(ed25519SpkiPrefix)) {
throw new Error("Unexpected public key format");
}

const ed25519Pubkey = spki.subarray(12); // 32 bytes

const addrWithSigner = generateAddressWithSigners({ rawEd25519Signer, ed25519Pubkey });
```

## Sharing Secrets and Multisig

It's common for an application to have multiple developers that can deploy changes to mainnet. It may be tempting to share a secret for a single account (manually or through a secrets manager), but this is **not recommended**. Instead, it is recommended to setup a multisig account between all the developers. The multisig account can be a 1/N threshold, which would still allow a single developer to make changes. The benefit of a multisig is that secrets do not need to be shared and all actions are immutably auditable on-chain. Each developer should then follow the practices outlined above.

```ts
const addrWithSigners = generateAddressWithSigners({ rawEd25519Signer: signer, ed25519Pubkey: pubkey });
const msigData: MultisigMetadata = {
version: 1,
threshold: 1,
addrs: [
otherSigner, // Address of the other signer
addrWithSigners.addr
],
}

const algorand = AlgorandClient.defaultLocalNet();

// Create a multisig account that can be used to sign as a 1/N signer
const msigAccount = new MultisigAccount(msigData, [addrWithSigners])

// Send a transaction using the multisig account
const pay = algorand.send.payment({
sender: msigAccount,
amount: microAlgos(0),
receiver: otherSigner,
})
```

## Key Rotation

Algorand has native support for key rotation through a feature called rekeying. Rekeying allows the blockchain address to stay the same while allowing for rotation of the underlying keypair. For example, a common pattern is to have an admin address that can deploy changes to a production contract. Rekeying allows the admin address to remain constant in the contract but allow the secrets used to authorize transactions to rotate. Rekeying can be done with any transaction type, but the simplest is to do a 0 ALGO payment to oneself with the rekeyTo field set.

```ts
const originalAddrWithSigners = generateAddressWithSigners({ rawEd25519Signer: originalSigner, ed25519Pubkey: originalPubkey });

const newAddrWithSigners = generateAddressWithSigners({
rawEd25519Signer: newSigner,
ed25519Pubkey: newPubkey,
// NOTE: We are specifying sendingAddress so we can properly sign transactions on behalf of the original address
sendingAddress: originalAddrWithSigners.addr,
});


const algorand = AlgorandClient.defaultLocalNet();

algorand.send.payment({
sender: originalAddrWithSigners,
amount: microAlgos(0),
receiver: originalAddrWithSigners,
rekeyTo: newAddrWithSigners,
})
```
5 changes: 5 additions & 0 deletions docs/src/loaders/examples-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ const CATEGORIES: Record<string, CategoryMeta> = {
description: 'Key Management Daemon operations for wallet and key management.',
slug: 'kmd-client',
},
signing: {
label: 'Signing',
description: 'Transaction signing with keyrings and cloud KMS providers.',
slug: 'signing',
},
testing: {
label: 'Testing',
description: 'Testing utilities for mock server setup and Vitest integration.',
Expand Down
12 changes: 11 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Runnable code examples demonstrating every major feature of the `@algorandfounda

## Overview

This folder contains 117 self-contained examples organized into 9 categories. Each example is a standalone TypeScript file that demonstrates specific functionality, progressing from basic to advanced usage within each category.
This folder contains 120 self-contained examples organized into 10 categories. Each example is a standalone TypeScript file that demonstrates specific functionality, progressing from basic to advanced usage within each category.

## Prerequisites

Expand Down Expand Up @@ -181,6 +181,16 @@ Key Management Daemon operations for wallet and key management.
| `12-program-signing.ts` | Create delegated logic signatures |
| `13-multisig-program-signing.ts` | Create delegated multisig logic signatures |

### Signing (`signing/`)

External signing providers and keyring integration for transaction signing.

| File | Description |
| ---------------------------- | ------------------------------------------------------ |
| `01-ed25519-from-keyring.ts` | Retrieve secrets from OS keyring and sign transactions |
| `02-hd-from-keyring.ts` | HD wallet signing using keyring-stored secrets |
| `03-aws-kms.ts` | Ed25519 signing using AWS KMS |

### Testing (`testing/`)

Testing utilities for mock server setup and Vitest integration. No LocalNet required.
Expand Down
Loading
Loading