Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/pushes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
go-version: ${{ matrix.go }}
# We're not doing releases, just checks, so we can live without check-latest here

- name: Export Go environment to Actions outputs
Expand Down
82 changes: 61 additions & 21 deletions keypair.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,17 @@ func CreatePairWithRand(prefix PrefixByte, rr io.Reader) (KeyPair, error) {
if prefix == PrefixByteCurve {
return CreateCurveKeysWithRand(rr)
}
_, priv, err := ed25519.GenerateKey(rr)
if err != nil {
return nil, err
}

seed, err := EncodeSeed(prefix, priv.Seed())
var (
seed []byte
err error
)
secretDo(func() {
var priv ed25519.PrivateKey
if _, priv, err = ed25519.GenerateKey(rr); err != nil {
return
}
seed, err = EncodeSeed(prefix, priv.Seed())
})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -79,15 +84,25 @@ func (pair *kp) Seed() ([]byte, error) {
// PublicKey will return the encoded public key associated with the KeyPair.
// All KeyPairs have a public key.
func (pair *kp) PublicKey() (string, error) {
public, raw, err := DecodeSeed(pair.seed)
if err != nil {
return "", err
}
pub, _, err := ed25519.GenerateKey(bytes.NewReader(raw))
var (
public PrefixByte
pub [ed25519.PublicKeySize]byte
err error
)
secretDo(func() {
var raw []byte
if public, raw, err = DecodeSeed(pair.seed); err != nil {
return
}
var pk ed25519.PublicKey
if pk, _, err = ed25519.GenerateKey(bytes.NewReader(raw)); err == nil {
copy(pub[:], pk)
}
})
if err != nil {
return "", err
}
pk, err := Encode(public, pub)
pk, err := Encode(public, pub[:])
if err != nil {
return "", err
}
Expand All @@ -96,29 +111,54 @@ func (pair *kp) PublicKey() (string, error) {

// PrivateKey will return the encoded private key for KeyPair.
func (pair *kp) PrivateKey() ([]byte, error) {
_, priv, err := pair.keys()
if err != nil {
return nil, err
}
return Encode(PrefixBytePrivate, priv)
var (
pk []byte
err error
)
secretDo(func() {
var priv ed25519.PrivateKey
if _, priv, err = pair.keys(); err != nil {
return
}
pk, err = Encode(PrefixBytePrivate, priv)
})
return pk, err
}

// Sign will sign the input with KeyPair's private key.
func (pair *kp) Sign(input []byte) ([]byte, error) {
_, priv, err := pair.keys()
// Signature is allocated outside of the secret scope since it is not sensitive.
sig := make([]byte, ed25519.SignatureSize)
var err error
secretDo(func() {
var priv ed25519.PrivateKey
if _, priv, err = pair.keys(); err != nil {
return
}
copy(sig, ed25519.Sign(priv, input))
})
if err != nil {
return nil, err
}
return ed25519.Sign(priv, input), nil
return sig, nil
}

// Verify will verify the input against a signature utilizing the public key.
func (pair *kp) Verify(input []byte, sig []byte) error {
pub, _, err := pair.keys()
var (
pub [ed25519.PublicKeySize]byte
err error
)
secretDo(func() {
var pk ed25519.PublicKey
if pk, _, err = pair.keys(); err == nil {
copy(pub[:], pk)
}
})
if err != nil {
return err
}
if !ed25519.Verify(pub, input, sig) {
if !ed25519.Verify(pub[:], input, sig) {
return ErrInvalidSignature
}
return nil
Expand Down
16 changes: 14 additions & 2 deletions nkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ func FromPublicKey(public string) (KeyPair, error) {

// FromSeed will create a KeyPair capable of signing and verifying signatures.
func FromSeed(seed []byte) (KeyPair, error) {
prefix, _, err := DecodeSeed(seed)
var (
prefix PrefixByte
err error
)
secretDo(func() {
prefix, _, err = DecodeSeed(seed)
})
if err != nil {
return nil, err
}
Expand All @@ -88,7 +94,13 @@ func FromSeed(seed []byte) (KeyPair, error) {

// FromRawSeed will create a KeyPair from the raw 32 byte seed for a given type.
func FromRawSeed(prefix PrefixByte, rawSeed []byte) (KeyPair, error) {
seed, err := EncodeSeed(prefix, rawSeed)
var (
seed []byte
err error
)
secretDo(func() {
seed, err = EncodeSeed(prefix, rawSeed)
})
if err != nil {
return nil, err
}
Expand Down
25 changes: 25 additions & 0 deletions secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build goexperiment.runtimesecret

package nkeys

import "runtime/secret"

// secretDo runs f via runtime/secret so that registers and stack used
// while handling private key material are erased before returning, and
// heap allocations made by f are erased once they become unreachable.
func secretDo(f func()) {
secret.Do(f)
}
21 changes: 21 additions & 0 deletions secret_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !goexperiment.runtimesecret

package nkeys

// secretDo simply invokes f when built without GOEXPERIMENT=runtimesecret.
func secretDo(f func()) {
f()
}
75 changes: 61 additions & 14 deletions xkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,45 +47,80 @@ func CreateCurveKeys() (KeyPair, error) {
// CreateCurveKeysWithRand will create a Curve typed KeyPair
// with specified rand source.
func CreateCurveKeysWithRand(rr io.Reader) (KeyPair, error) {
var kp ckp
_, priv, err := ed25519.GenerateKey(rr)
var (
kp ckp
err error
)
secretDo(func() {
var priv ed25519.PrivateKey
if _, priv, err = ed25519.GenerateKey(rr); err == nil {
kp.seed = [curveKeyLen]byte(priv.Seed())
}
})
if err != nil {
return nil, err
}
kp.seed = [curveKeyLen]byte(priv.Seed())
return &kp, nil
}

// Will create a curve key pair from seed.
func FromCurveSeed(seed []byte) (KeyPair, error) {
pb, raw, err := DecodeSeed(seed)
var (
kp ckp
err error
)
secretDo(func() {
var (
pb PrefixByte
raw []byte
)
if pb, raw, err = DecodeSeed(seed); err != nil {
return
}
if pb != PrefixByteCurve || len(raw) != curveKeyLen {
err = ErrInvalidCurveSeed
return
}
copy(kp.seed[:], raw)
})
if err != nil {
return nil, err
}
if pb != PrefixByteCurve || len(raw) != curveKeyLen {
return nil, ErrInvalidCurveSeed
}
var kp ckp
copy(kp.seed[:], raw)
return &kp, nil
}

// Seed will return the encoded seed.
func (pair *ckp) Seed() ([]byte, error) {
return EncodeSeed(PrefixByteCurve, pair.seed[:])
var (
seed []byte
err error
)
secretDo(func() {
seed, err = EncodeSeed(PrefixByteCurve, pair.seed[:])
})
return seed, err
}

// PublicKey will return the encoded public key.
func (pair *ckp) PublicKey() (string, error) {
var pub [curveKeyLen]byte
curve25519.ScalarBaseMult(&pub, &pair.seed)
secretDo(func() {
curve25519.ScalarBaseMult(&pub, &pair.seed)
})
key, err := Encode(PrefixByteCurve, pub[:])
return string(key), err
}

// PrivateKey will return the encoded private key.
func (pair *ckp) PrivateKey() ([]byte, error) {
return Encode(PrefixBytePrivate, pair.seed[:])
var (
pk []byte
err error
)
secretDo(func() {
pk, err = Encode(PrefixBytePrivate, pair.seed[:])
})
return pk, err
}

func decodePubCurveKey(src string, dest []byte) error {
Expand Down Expand Up @@ -145,7 +180,13 @@ func (pair *ckp) SealWithRand(input []byte, recipient string, rr io.Reader) ([]b
}
copy(out[:vlen], []byte(XKeyVersionV1))
copy(out[vlen:], nonce[:])
return box.Seal(out[:], input, &nonce, &rpub, &pair.seed), nil
// Size the output outside of the secret scope since it is not sensitive.
sealed := make([]byte, 0, len(out)+len(input)+box.Overhead)
sealed = append(sealed, out[:]...)
secretDo(func() {
sealed = box.Seal(sealed, input, &nonce, &rpub, &pair.seed)
})
return sealed, nil
}

func (pair *ckp) Open(input []byte, sender string) ([]byte, error) {
Expand All @@ -166,7 +207,13 @@ func (pair *ckp) Open(input []byte, sender string) ([]byte, error) {
return nil, ErrInvalidSender
}

decrypted, ok := box.Open(nil, input[vlen+curveNonceLen:], &nonce, &spub, &pair.seed)
var (
decrypted []byte
ok bool
)
secretDo(func() {
decrypted, ok = box.Open(nil, input[vlen+curveNonceLen:], &nonce, &spub, &pair.seed)
})
if !ok {
return nil, ErrCouldNotDecrypt
}
Expand Down
Loading