diff --git a/.github/workflows/pushes.yaml b/.github/workflows/pushes.yaml index b661a1c..1f16982 100644 --- a/.github/workflows/pushes.yaml +++ b/.github/workflows/pushes.yaml @@ -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 diff --git a/keypair.go b/keypair.go index 69ebe21..13b7626 100644 --- a/keypair.go +++ b/keypair.go @@ -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 } @@ -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 } @@ -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 diff --git a/nkeys.go b/nkeys.go index 968b5e9..97d19ec 100644 --- a/nkeys.go +++ b/nkeys.go @@ -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 } @@ -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 } diff --git a/secret.go b/secret.go new file mode 100644 index 0000000..254984b --- /dev/null +++ b/secret.go @@ -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) +} diff --git a/secret_stub.go b/secret_stub.go new file mode 100644 index 0000000..5aa7dc3 --- /dev/null +++ b/secret_stub.go @@ -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() +} diff --git a/xkeys.go b/xkeys.go index 7951fb7..96b91e4 100644 --- a/xkeys.go +++ b/xkeys.go @@ -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 { @@ -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) { @@ -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 }