feat: harden support, localize knowledge and add operator availability [skip ci]

Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.

Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.

Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.

Expected source tree: 0a4a7528e9.
This commit is contained in:
2026-10-16 15:21:42 +00:00
parent 299ea50fb5
commit 2f7a491666
176 changed files with 11869 additions and 2716 deletions

View File

@@ -1,3 +1,5 @@
// Modified in the independent hardening fork; see docs/hardening.
package openidentity
import (
@@ -269,3 +271,33 @@ func flipLastChar(signature string) string {
func itoa(value int64) string {
return strconv.FormatInt(value, 10)
}
func TestRequireSignedIdentityRejectsUnsignedAndInvalidSignatures(t *testing.T) {
for _, signature := range []string{"", "invalid"} {
ctx, _ := newIdentityContext(map[string]string{"X-External-Id": testUserID, "X-External-Id-Signature": signature})
if _, err := GetExternalUser(ctx, Secrets{ChannelID: testChannelID, IdentityHMAC: testSecret, RequireSignedIdentity: true}); err == nil {
t.Fatal("signed-only policy accepted an unsigned or invalid identity")
}
}
ctx, _ := newIdentityContext(signedIdentityHeaders(testSecret, testChannelID, testUserID, time.Now()))
user, err := GetExternalUser(ctx, Secrets{ChannelID: testChannelID, IdentityHMAC: testSecret, RequireSignedIdentity: true})
if err != nil || user.IdentityScope != IdentityScope(testChannelID, "hmac") {
t.Fatal("valid signed identity did not retain its HMAC scope")
}
}
func TestIdentityScopeSeparatesIssuersAndChannels(t *testing.T) {
seen := map[string]bool{}
for _, channel := range []string{"channel-one", "channel-two"} {
for _, issuer := range []string{"guest", "hmac", "jwt"} {
scope := IdentityScope(channel, issuer)
if scope == "" || seen[scope] {
t.Fatal("identity scopes collided")
}
seen[scope] = true
}
}
if IdentityScope("", "hmac") != "" || IdentityScope("channel", "unknown") != "" {
t.Fatal("invalid scope inputs accepted")
}
}

View File

@@ -1,3 +1,5 @@
// Modified in the independent hardening fork; see docs/hardening.
package openidentity
import (
@@ -24,6 +26,9 @@ type ExternalUser struct {
ExternalSource enums.ExternalSource `json:"externalSource"`
ExternalID string `json:"externalId"`
ExternalName string `json:"externalName"`
// IdentityScope and ChannelID are server-established authorization context, never request fields.
IdentityScope string `json:"-"`
ChannelID int64 `json:"-"`
}
type UserTokenClaims struct {
@@ -46,7 +51,8 @@ type Secrets struct {
IdentityMaxAge time.Duration
// ChannelID is bound into the signed payload so a signature obtained for one
// channel cannot be replayed on another.
ChannelID string
ChannelID string
RequireSignedIdentity bool
}
// DefaultIdentityMaxAge is how long a signed visitor identity stays acceptable
@@ -65,9 +71,8 @@ const identityFutureSkewTolerance = 5 * time.Minute
//
// issuedAt is unix seconds. Lower-case hex output. Three details are deliberate:
//
// - The channel id is included because CustomerIdentity is keyed by
// (source, externalId) globally, so without it a signature minted for one
// channel would assert the same identity on every other channel.
// - The channel id is included so signatures cannot be replayed on a
// different channel, even when a deployment shares an HMAC signing key.
// - The timestamp is inside the signed bytes even though it also travels as its
// own field, so it cannot be re-dated without breaking the signature. This
// bounds how long a leaked pair stays usable; it is not a defence against key
@@ -126,6 +131,7 @@ func GetExternalUser(ctx *gin.Context, secrets Secrets) (*ExternalUser, error) {
ExternalSource: enums.ExternalSourceUser,
ExternalID: claims.UserID,
ExternalName: claims.Name,
IdentityScope: IdentityScope(secrets.ChannelID, "jwt"),
}, nil
}
return getExternalIDUser(ctx, secrets)
@@ -201,13 +207,18 @@ func getExternalIDUser(ctx *gin.Context, secrets Secrets) (*ExternalUser, error)
ExternalSource: enums.ExternalSourceUser,
ExternalID: externalID,
ExternalName: externalName,
IdentityScope: IdentityScope(secrets.ChannelID, "hmac"),
}, nil
}
if secrets.RequireSignedIdentity {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
return &ExternalUser{
ExternalSource: enums.ExternalSourceGuest,
ExternalID: externalID,
ExternalName: externalName,
IdentityScope: IdentityScope(secrets.ChannelID, "guest"),
}, nil
}
@@ -267,3 +278,30 @@ func decodeExternalDisplayName(s string) string {
}
return strings.TrimSpace(dec)
}
// IdentityScope separates independent identity issuers, trust levels and channels.
// It deliberately does not depend on a secret so ordinary key rotation preserves
// the issuer's history. Reassigning a channel to a different issuer requires a new
// channel, not silently reusing that issuer's namespace.
func IdentityScope(channelCode, issuer string) string {
if strings.TrimSpace(channelCode) == "" {
return ""
}
switch issuer {
case "jwt", "hmac", "guest":
default:
return ""
}
digest := sha256.Sum256([]byte(strings.TrimSpace(channelCode)))
return "v1:" + issuer + ":" + hex.EncodeToString(digest[:])
}
func ValidIdentityScope(channelCode string, external ExternalUser) bool {
if external.ExternalSource == enums.ExternalSourceGuest {
return external.IdentityScope != "" && external.IdentityScope == IdentityScope(channelCode, "guest")
}
if external.ExternalSource == enums.ExternalSourceUser {
return external.IdentityScope != "" && (external.IdentityScope == IdentityScope(channelCode, "jwt") || external.IdentityScope == IdentityScope(channelCode, "hmac"))
}
return false
}