Skip to content
Merged
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
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ require (
github.com/fil-forge/swarf v0.0.1-0.20260821055917-4921bc49627c
github.com/fil-forge/ucantone v0.0.0-20260817170631-3a20cd59fabc
github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a
github.com/filecoin-project/go-fee v0.1.1-0.20260818173301-72a3a35f83ef
github.com/gofiber/fiber/v3 v3.5.0
github.com/google/uuid v1.6.0
github.com/ipfs/go-block-format v0.2.4
Expand All @@ -27,6 +26,7 @@ require (
github.com/multiformats/go-multicodec v0.10.0
github.com/multiformats/go-multihash v0.2.3
github.com/oklog/ulid/v2 v2.1.2
github.com/openbao/openbao/api/v2 v2.6.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pressly/goose/v3 v3.27.2
github.com/spf13/cobra v1.10.2
Expand All @@ -37,7 +37,6 @@ require (
go.uber.org/fx v1.24.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.28.0
golang.org/x/sys v0.47.0
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
)

Expand Down Expand Up @@ -119,8 +118,11 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/hashicorp/vault-client-go v0.4.3 // indirect
github.com/in-toto/attestation v1.2.0 // indirect
github.com/in-toto/in-toto-golang v0.11.0 // indirect
Expand Down Expand Up @@ -148,6 +150,7 @@ require (
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/buildkit v0.31.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
Expand Down Expand Up @@ -243,6 +246,7 @@ require (
golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
Expand Down
18 changes: 14 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fil-forge/automobile v0.0.1 h1:9xB3yc4l5b9EdRJSJcNwudgBFNHoMPEAdcb7GfobLhA=
Expand All @@ -193,8 +193,6 @@ github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a h1:lDwnNmF4LNb
github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a/go.mod h1:t73Wa2xqpT0NdY+SzRZDiO+tQD13b3UEZRoe2wYFiIE=
github.com/filecoin-project/go-data-segment v0.0.1 h1:1wmDxOG4ubWQm3ZC1XI5nCon5qgSq7Ra3Rb6Dbu10Gs=
github.com/filecoin-project/go-data-segment v0.0.1/go.mod h1:H0/NKbsRxmRFBcLibmABv+yFNHdmtl5AyplYLnb0Zv4=
github.com/filecoin-project/go-fee v0.1.1-0.20260818173301-72a3a35f83ef h1:o45NsZZct1bbwdfBKFZyWiHa7jlK3nGR4pNmr1ioBys=
github.com/filecoin-project/go-fee v0.1.1-0.20260818173301-72a3a35f83ef/go.mod h1:A+3InT4C3mn8B10ZQ+ySX7hxjYxN9MlsoF/87RuIUvg=
github.com/filecoin-project/go-fil-commcid v0.3.1 h1:4EfxpHSlvtkOqa9weG2Yt5kxFmPib2xU7Uc9Lbqk7fs=
github.com/filecoin-project/go-fil-commcid v0.3.1/go.mod h1:z7Ssf8d7kspF9QRAVHDbZ+43JK4mkhbGH5lyph1TnKY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
Expand Down Expand Up @@ -265,6 +263,8 @@ github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
github.com/go-openapi/validate v0.25.3 h1:4nzAIavcJ7WveHK2+V1UAkZK3kWcjzxZCzjfZAfavKs=
github.com/go-openapi/validate v0.25.3/go.mod h1:GemfuGMyYpIaBoKpX3z8sLywrmxpzWVOoJ7R0VeAVuk=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
Expand Down Expand Up @@ -313,12 +313,18 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk=
Expand Down Expand Up @@ -411,6 +417,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.31.0 h1:hMUAbQGgjtzJDDOZ6o7MQk5XBZkBTyzLWEvnjguHHQI=
github.com/moby/buildkit v0.31.0/go.mod h1:YM5iNEbNCc6L1Zt3YWFB/aXNLufvf4Rcu0DPlc9HwQg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
Expand Down Expand Up @@ -475,6 +483,8 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec=
github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/openbao/openbao/api/v2 v2.6.0 h1:KvfspAaL9bab9hI8jFYkV2cgtSrwWtaG+k9AUTHWU4M=
github.com/openbao/openbao/api/v2 v2.6.0/go.mod h1:H4IWiH+2rgF/TbrsUbsfrMyGoqojkLqxPCRLENSMnSo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
Expand Down
163 changes: 163 additions & 0 deletions regionkey/inprocess.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package regionkey

import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"sync"
)

// KEKLen is the required length in bytes of a region KEK: 32 bytes (AES-256).
const KEKLen = 32

// gcmNonceLen is the standard 12-byte AES-GCM nonce length. A wrap generates
// a fresh random nonce and carries it as the first bytes of the wrapped
// ciphertext.
const gcmNonceLen = 12

// InProcessProvider is the in-process region [Provider] for tests and
// development: AES-256-GCM under a per-binding subkey, mirroring the
// semantics of the production secrets-manager wrap (OpenBao transit
// aes256-gcm96 with derived=true, context bound to (space, blob digest)) so
// code exercised against it sees the same properties — most importantly that
// a wrap authenticates only against its own binding. It holds raw KEK bytes in
// ordinary process memory, which a production provider never does: per the
// regional security RFC, real key custody belongs to the secrets manager,
// which implements [Provider] directly and never exports a key. That is also
// why there is no key-custody seam here — this provider is the only
// implementation that would ever consume one.
//
// The provider keeps every key version it has ever held (archive-don't-
// destroy): [InProcessProvider.Rotate] makes a new version current for new
// wraps while existing wraps keep unwrapping under theirs — the encryption
// RFC's rotation-readiness groundwork. Every binding resolves to the single
// current key; the region-key cardinality decision (FIL-572) is deferred.
type InProcessProvider struct {
mu sync.RWMutex
current KeyVersion
keys map[KeyVersion][]byte
}

var _ Provider = (*InProcessProvider)(nil)

// NewInProcessProvider returns a provider holding kek as the current region
// key under version. kek must be exactly [KEKLen] bytes and version must be
// non-empty. kek is copied; the caller's slice is not retained.
func NewInProcessProvider(version KeyVersion, kek []byte) (*InProcessProvider, error) {
if err := validateKey(version, kek); err != nil {
return nil, err
}
return &InProcessProvider{
current: version,
keys: map[KeyVersion][]byte{version: bytes.Clone(kek)},
}, nil
}

// Rotate makes kek, under version, the current key for new wraps and archives
// the previous version so existing wraps still unwrap — the archive-don't-
// destroy rotation the RFC's groundwork calls for. It rejects a version the
// provider already holds, and the same invalid inputs the constructor does.
func (p *InProcessProvider) Rotate(version KeyVersion, kek []byte) error {
if err := validateKey(version, kek); err != nil {
return err
}
p.mu.Lock()
defer p.mu.Unlock()
if _, ok := p.keys[version]; ok {
return fmt.Errorf("regionkey: key version %q already exists", version)
}
p.keys[version] = bytes.Clone(kek)
p.current = version
return nil
}

// validateKey enforces the constructor/Rotate input contract: a non-empty
// version naming a KEKLen-byte key.
func validateKey(version KeyVersion, kek []byte) error {
if version == "" {
return errors.New("regionkey: key version must not be empty")
}
if len(kek) != KEKLen {
return fmt.Errorf("regionkey: region KEK must be %d bytes (AES-256), got %d", KEKLen, len(kek))
}
return nil
}

// Wrap implements [Provider.Wrap]: it seals cek with AES-256-GCM under the
// binding-derived subkey of the current region KEK, prepends the fresh random
// nonce, and tags the result with the KEK's version.
func (p *InProcessProvider) Wrap(_ context.Context, binding BindingContext, cek []byte) (WrappedKey, error) {
p.mu.RLock()
version := p.current
kek := p.keys[version]
p.mu.RUnlock()

aead, err := boundAEAD(kek, binding)
if err != nil {
return WrappedKey{}, err
}
nonce := make([]byte, gcmNonceLen)
if _, err := rand.Read(nonce); err != nil {
return WrappedKey{}, fmt.Errorf("regionkey: generating nonce: %w", err)
}
return WrappedKey{Version: version, Ciphertext: aead.Seal(nonce, nonce, cek, nil)}, nil
}
Comment on lines +92 to +110

// Unwrap implements [Provider.Unwrap]: it derives the same binding-bound
// subkey from the KEK named by wrapped.Version and opens the ciphertext. A
// wrong key, a wrong binding, and tampered bytes all fail authentication and
// surface as an error wrapping [ErrAuthentication]; a version the provider
// does not hold surfaces as [ErrUnknownVersion].
func (p *InProcessProvider) Unwrap(_ context.Context, binding BindingContext, wrapped WrappedKey) ([]byte, error) {
p.mu.RLock()
kek, ok := p.keys[wrapped.Version]
p.mu.RUnlock()
if !ok {
return nil, fmt.Errorf("regionkey: fetching KEK version %q: %w", wrapped.Version, ErrUnknownVersion)
}

aead, err := boundAEAD(kek, binding)
if err != nil {
return nil, err
}
if len(wrapped.Ciphertext) < gcmNonceLen {
return nil, fmt.Errorf("regionkey: wrapped CEK shorter than a nonce: %w", ErrAuthentication)
}
nonce, sealed := wrapped.Ciphertext[:gcmNonceLen], wrapped.Ciphertext[gcmNonceLen:]
cek, err := aead.Open(nil, nonce, sealed, nil)
if err != nil {
return nil, fmt.Errorf("regionkey: unwrapping CEK: %w", ErrAuthentication)
}
return cek, nil
}

// boundAEAD builds the AES-256-GCM AEAD for one (KEK, binding) pair. The
// subkey is HKDF-SHA256(kek, info=binding), so every binding gets its own GCM
// key — the software analogue of OpenBao transit's derived=true context
// binding, and what makes a wrong-binding unwrap an authentication failure.
// The KEK length check restates the constructor/Rotate invariant as a cheap
// guard.
func boundAEAD(kek []byte, binding BindingContext) (cipher.AEAD, error) {
if len(kek) != KEKLen {
return nil, fmt.Errorf("regionkey: KEK is %d bytes, want %d", len(kek), KEKLen)
}
subkey, err := hkdf.Key(sha256.New, kek, nil, string(bindingBytes(binding)), KEKLen)
if err != nil {
return nil, fmt.Errorf("regionkey: deriving binding subkey: %w", err)
}
block, err := aes.NewCipher(subkey)
if err != nil {
return nil, fmt.Errorf("regionkey: building cipher: %w", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("regionkey: building GCM: %w", err)
}
return aead, nil
}
111 changes: 0 additions & 111 deletions regionkey/kek.go

This file was deleted.

Loading
Loading