From 2ee88de9a0b9426fe1963c96de8ac61b55de8476 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Wed, 20 May 2026 14:56:40 +0100 Subject: [PATCH 1/3] feat(secureservice): add admission primitives Introduce provider-neutral admission config and verifier types for future federated network admission support. The new config is disabled by default and does not change handshake behavior. --- net/secureservice/admission.go | 75 +++++++++++++++++++++++++++ net/secureservice/admission_test.go | 78 +++++++++++++++++++++++++++++ net/secureservice/config.go | 5 +- 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 net/secureservice/admission.go create mode 100644 net/secureservice/admission_test.go diff --git a/net/secureservice/admission.go b/net/secureservice/admission.go new file mode 100644 index 00000000..a788f746 --- /dev/null +++ b/net/secureservice/admission.go @@ -0,0 +1,75 @@ +package secureservice + +import "context" + +const ( + DefaultAdmissionIdentityClaim = "anytype_identity" + DefaultAdmissionNetworkClaim = "network_id" + DefaultAdmissionSubjectClaim = "sub" + DefaultAdmissionClockSkewSec = 60 +) + +type AdmissionConfig struct { + Enabled bool `yaml:"enabled"` + Required bool `yaml:"required"` + Issuer string `yaml:"issuer"` + Audience string `yaml:"audience"` + JWKSURL string `yaml:"jwksUrl"` + RequiredClaims map[string]any `yaml:"requiredClaims"` + IdentityClaim string `yaml:"identityClaim"` + NetworkClaim string `yaml:"networkClaim"` + SubjectClaim string `yaml:"subjectClaim"` + ClockSkewSec int `yaml:"clockSkewSec"` +} + +func (c AdmissionConfig) WithDefaults() AdmissionConfig { + if c.IdentityClaim == "" { + c.IdentityClaim = DefaultAdmissionIdentityClaim + } + if c.NetworkClaim == "" { + c.NetworkClaim = DefaultAdmissionNetworkClaim + } + if c.SubjectClaim == "" { + c.SubjectClaim = DefaultAdmissionSubjectClaim + } + if c.ClockSkewSec == 0 { + c.ClockSkewSec = DefaultAdmissionClockSkewSec + } + return c +} + +type AdmissionRequest struct { + Token string + Identity []byte + NetworkID string + PeerID string + ClientVersion string +} + +type AdmissionClaims struct { + Subject string + Issuer string + Audience []string + NetworkID string + Identity []byte + Claims map[string]any +} + +type AdmissionDecision struct { + Allowed bool + Reason string + Claims AdmissionClaims +} + +type AdmissionVerifier interface { + VerifyAdmission(ctx context.Context, req AdmissionRequest) (AdmissionDecision, error) +} + +type NoopAdmissionVerifier struct{} + +func (NoopAdmissionVerifier) VerifyAdmission(ctx context.Context, req AdmissionRequest) (AdmissionDecision, error) { + return AdmissionDecision{ + Allowed: true, + Reason: "admission disabled", + }, nil +} diff --git a/net/secureservice/admission_test.go b/net/secureservice/admission_test.go new file mode 100644 index 00000000..8a59ca05 --- /dev/null +++ b/net/secureservice/admission_test.go @@ -0,0 +1,78 @@ +package secureservice + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestAdmissionConfig_WithDefaults(t *testing.T) { + conf := AdmissionConfig{}.WithDefaults() + + assert.False(t, conf.Enabled) + assert.False(t, conf.Required) + assert.Equal(t, DefaultAdmissionIdentityClaim, conf.IdentityClaim) + assert.Equal(t, DefaultAdmissionNetworkClaim, conf.NetworkClaim) + assert.Equal(t, DefaultAdmissionSubjectClaim, conf.SubjectClaim) + assert.Equal(t, DefaultAdmissionClockSkewSec, conf.ClockSkewSec) +} + +func TestAdmissionConfig_WithDefaultsPreservesConfiguredValues(t *testing.T) { + conf := AdmissionConfig{ + Enabled: true, + Required: true, + IdentityClaim: "identity", + NetworkClaim: "network", + SubjectClaim: "subject", + ClockSkewSec: 120, + }.WithDefaults() + + assert.True(t, conf.Enabled) + assert.True(t, conf.Required) + assert.Equal(t, "identity", conf.IdentityClaim) + assert.Equal(t, "network", conf.NetworkClaim) + assert.Equal(t, "subject", conf.SubjectClaim) + assert.Equal(t, 120, conf.ClockSkewSec) +} + +func TestAdmissionConfig_YAML(t *testing.T) { + var conf Config + err := yaml.Unmarshal([]byte(` +admission: + enabled: true + required: true + issuer: https://issuer.example + audience: any-sync + jwksUrl: https://issuer.example/.well-known/jwks.json + requiredClaims: + group: docs-users + identityClaim: anytype_id + networkClaim: sync_network + subjectClaim: user + clockSkewSec: 30 +`), &conf) + require.NoError(t, err) + + assert.True(t, conf.Admission.Enabled) + assert.True(t, conf.Admission.Required) + assert.Equal(t, "https://issuer.example", conf.Admission.Issuer) + assert.Equal(t, "any-sync", conf.Admission.Audience) + assert.Equal(t, "https://issuer.example/.well-known/jwks.json", conf.Admission.JWKSURL) + assert.Equal(t, map[string]any{"group": "docs-users"}, conf.Admission.RequiredClaims) + assert.Equal(t, "anytype_id", conf.Admission.IdentityClaim) + assert.Equal(t, "sync_network", conf.Admission.NetworkClaim) + assert.Equal(t, "user", conf.Admission.SubjectClaim) + assert.Equal(t, 30, conf.Admission.ClockSkewSec) +} + +func TestNoopAdmissionVerifier_AllowsRequests(t *testing.T) { + verifier := NoopAdmissionVerifier{} + decision, err := verifier.VerifyAdmission(context.Background(), AdmissionRequest{}) + + require.NoError(t, err) + assert.True(t, decision.Allowed) + assert.Equal(t, "admission disabled", decision.Reason) +} diff --git a/net/secureservice/config.go b/net/secureservice/config.go index 7d5eb3ae..44a841ce 100644 --- a/net/secureservice/config.go +++ b/net/secureservice/config.go @@ -13,8 +13,9 @@ type configGetter interface { } type Config struct { - RequireClientAuth bool `yaml:"requireClientAuth"` - CompatibleVersions []uint32 `yaml:"compatibleVersions"` + RequireClientAuth bool `yaml:"requireClientAuth"` + CompatibleVersions []uint32 `yaml:"compatibleVersions"` + Admission AdmissionConfig `yaml:"admission"` } // CtxAllowAccountCheck upgrades the context to allow identity check on handshake From 11be3faef84dafcbbbfee9e839795ca890f3a42d Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Wed, 20 May 2026 15:07:28 +0100 Subject: [PATCH 2/3] feat(secureservice): add JWT admission verifier Add a static JWKS-backed AdmissionVerifier implementation for provider-neutral federated admission. The verifier validates token signature, issuer, audience, expiry, network id, Anytype identity binding, subject, and required claims without wiring it into the handshake yet. --- net/secureservice/admission_jwt.go | 495 ++++++++++++++++++++++++ net/secureservice/admission_jwt_test.go | 227 +++++++++++ 2 files changed, 722 insertions(+) create mode 100644 net/secureservice/admission_jwt.go create mode 100644 net/secureservice/admission_jwt_test.go diff --git a/net/secureservice/admission_jwt.go b/net/secureservice/admission_jwt.go new file mode 100644 index 00000000..2d39bd71 --- /dev/null +++ b/net/secureservice/admission_jwt.go @@ -0,0 +1,495 @@ +package secureservice + +import ( + "bytes" + "context" + stdcrypto "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/big" + "strings" + "time" + + anycrypto "github.com/anyproto/any-sync/util/crypto" +) + +var ( + // ErrAdmissionDenied is returned when an admission request is rejected. + ErrAdmissionDenied = errors.New("admission denied") + // ErrAdmissionInvalidConfig is returned when JWT admission verifier configuration is incomplete or invalid. + ErrAdmissionInvalidConfig = errors.New("invalid admission config") + // ErrAdmissionInvalidToken is returned when an admission token is malformed or fails validation. + ErrAdmissionInvalidToken = errors.New("invalid admission token") +) + +type jwtAdmissionVerifier struct { + conf AdmissionConfig + keys map[string]jwtPublicKey + now func() time.Time +} + +type jwtPublicKey struct { + algorithm string + key any +} + +type jwtHeader struct { + Algorithm string `json:"alg"` + KeyID string `json:"kid"` +} + +type jsonWebKeySet struct { + Keys []jsonWebKey `json:"keys"` +} + +type jsonWebKey struct { + KeyType string `json:"kty"` + KeyID string `json:"kid"` + Use string `json:"use"` + Algorithm string `json:"alg"` + N string `json:"n"` + E string `json:"e"` + Curve string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` +} + +// NewJWTAdmissionVerifier creates an AdmissionVerifier that validates JWTs against a static JWKS document. +func NewJWTAdmissionVerifier(conf AdmissionConfig, jwks []byte) (AdmissionVerifier, error) { + return newJWTAdmissionVerifier(conf, jwks, time.Now) +} + +func newJWTAdmissionVerifier(conf AdmissionConfig, jwks []byte, now func() time.Time) (*jwtAdmissionVerifier, error) { + conf = conf.WithDefaults() + if conf.Issuer == "" || conf.Audience == "" || conf.IdentityClaim == "" || conf.NetworkClaim == "" || conf.SubjectClaim == "" { + return nil, ErrAdmissionInvalidConfig + } + keys, err := parseJWKS(jwks) + if err != nil { + return nil, err + } + if len(keys) == 0 { + return nil, ErrAdmissionInvalidConfig + } + if now == nil { + now = time.Now + } + return &jwtAdmissionVerifier{ + conf: conf, + keys: keys, + now: now, + }, nil +} + +func (v *jwtAdmissionVerifier) VerifyAdmission(ctx context.Context, req AdmissionRequest) (AdmissionDecision, error) { + if err := ctx.Err(); err != nil { + return AdmissionDecision{Allowed: false, Reason: err.Error()}, err + } + if req.Token == "" || len(req.Identity) == 0 || req.NetworkID == "" { + return v.deny("missing admission token, identity, or network id", ErrAdmissionInvalidToken) + } + + header, claims, signingInput, signature, err := parseJWT(req.Token) + if err != nil { + return v.deny("malformed admission token", err) + } + key, ok := v.keys[header.KeyID] + if !ok { + return v.deny("unknown signing key", ErrAdmissionInvalidToken) + } + if err = verifyJWTSignature(header.Algorithm, key, signingInput, signature); err != nil { + return v.deny("invalid admission token signature", err) + } + + decisionClaims, err := v.validateClaims(req, claims) + if err != nil { + return v.deny(err.Error(), err) + } + return AdmissionDecision{ + Allowed: true, + Reason: "admission allowed", + Claims: decisionClaims, + }, nil +} + +func (v *jwtAdmissionVerifier) deny(reason string, err error) (AdmissionDecision, error) { + return AdmissionDecision{Allowed: false, Reason: reason}, errors.Join(ErrAdmissionDenied, err) +} + +func parseJWT(token string) (jwtHeader, map[string]any, []byte, []byte, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + signature, err := base64.RawURLEncoding.DecodeString(parts[2]) + if err != nil { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + + var header jwtHeader + if err = decodeJSON(headerBytes, &header); err != nil { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + if header.Algorithm == "" || header.KeyID == "" { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + + claims := map[string]any{} + if err = decodeJSON(payloadBytes, &claims); err != nil { + return jwtHeader{}, nil, nil, nil, ErrAdmissionInvalidToken + } + return header, claims, []byte(parts[0] + "." + parts[1]), signature, nil +} + +func decodeJSON(data []byte, v any) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + return dec.Decode(v) +} + +func parseJWKS(jwks []byte) (map[string]jwtPublicKey, error) { + var set jsonWebKeySet + if err := decodeJSON(jwks, &set); err != nil { + return nil, ErrAdmissionInvalidConfig + } + keys := make(map[string]jwtPublicKey, len(set.Keys)) + for _, key := range set.Keys { + if key.KeyID == "" || key.Use == "enc" { + continue + } + parsed, err := parseJWK(key) + if err != nil { + return nil, err + } + keys[key.KeyID] = parsed + } + return keys, nil +} + +func parseJWK(key jsonWebKey) (jwtPublicKey, error) { + switch key.KeyType { + case "RSA": + return parseRSAJWK(key) + case "EC": + return parseECJWK(key) + case "OKP": + return parseOKPJWK(key) + default: + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } +} + +func parseRSAJWK(key jsonWebKey) (jwtPublicKey, error) { + n, err := base64.RawURLEncoding.DecodeString(key.N) + if err != nil { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + e, err := base64.RawURLEncoding.DecodeString(key.E) + if err != nil { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + exponent := int(new(big.Int).SetBytes(e).Int64()) + if exponent == 0 || len(n) == 0 { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + return jwtPublicKey{ + algorithm: key.Algorithm, + key: &rsa.PublicKey{ + N: new(big.Int).SetBytes(n), + E: exponent, + }, + }, nil +} + +func parseECJWK(key jsonWebKey) (jwtPublicKey, error) { + x, err := base64.RawURLEncoding.DecodeString(key.X) + if err != nil { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + y, err := base64.RawURLEncoding.DecodeString(key.Y) + if err != nil { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + curve := curveForJWK(key.Curve) + xCoord := new(big.Int).SetBytes(x) + yCoord := new(big.Int).SetBytes(y) + if curve == nil || len(x) == 0 || len(y) == 0 || !curve.IsOnCurve(xCoord, yCoord) { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + return jwtPublicKey{ + algorithm: key.Algorithm, + key: &ecdsa.PublicKey{ + Curve: curve, + X: xCoord, + Y: yCoord, + }, + }, nil +} + +func curveForJWK(curve string) elliptic.Curve { + switch curve { + case "P-256": + return elliptic.P256() + case "P-384": + return elliptic.P384() + case "P-521": + return elliptic.P521() + default: + return nil + } +} + +func parseOKPJWK(key jsonWebKey) (jwtPublicKey, error) { + if key.Curve != "Ed25519" { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + x, err := base64.RawURLEncoding.DecodeString(key.X) + if err != nil || len(x) != ed25519.PublicKeySize { + return jwtPublicKey{}, ErrAdmissionInvalidConfig + } + return jwtPublicKey{ + algorithm: key.Algorithm, + key: ed25519.PublicKey(x), + }, nil +} + +func verifyJWTSignature(algorithm string, key jwtPublicKey, signingInput []byte, signature []byte) error { + if key.algorithm != "" && key.algorithm != algorithm { + return ErrAdmissionInvalidToken + } + switch algorithm { + case "RS256": + digest := sha256.Sum256(signingInput) + return verifyRSA(key.key, stdcrypto.SHA256, digest[:], signature) + case "RS384": + digest := sha512.Sum384(signingInput) + return verifyRSA(key.key, stdcrypto.SHA384, digest[:], signature) + case "RS512": + digest := sha512.Sum512(signingInput) + return verifyRSA(key.key, stdcrypto.SHA512, digest[:], signature) + case "ES256": + digest := sha256.Sum256(signingInput) + return verifyECDSA(key.key, digest[:], signature, 32) + case "ES384": + digest := sha512.Sum384(signingInput) + return verifyECDSA(key.key, digest[:], signature, 48) + case "ES512": + digest := sha512.Sum512(signingInput) + return verifyECDSA(key.key, digest[:], signature, 66) + case "EdDSA": + pub, ok := key.key.(ed25519.PublicKey) + if !ok || !ed25519.Verify(pub, signingInput, signature) { + return ErrAdmissionInvalidToken + } + return nil + default: + return ErrAdmissionInvalidToken + } +} + +func verifyRSA(key any, hash stdcrypto.Hash, digest []byte, signature []byte) error { + pub, ok := key.(*rsa.PublicKey) + if !ok { + return ErrAdmissionInvalidToken + } + if err := rsa.VerifyPKCS1v15(pub, hash, digest, signature); err != nil { + return ErrAdmissionInvalidToken + } + return nil +} + +func verifyECDSA(key any, digest []byte, signature []byte, keyBytes int) error { + pub, ok := key.(*ecdsa.PublicKey) + if !ok || len(signature) != keyBytes*2 { + return ErrAdmissionInvalidToken + } + r := new(big.Int).SetBytes(signature[:keyBytes]) + s := new(big.Int).SetBytes(signature[keyBytes:]) + if !ecdsa.Verify(pub, digest, r, s) { + return ErrAdmissionInvalidToken + } + return nil +} + +func (v *jwtAdmissionVerifier) validateClaims(req AdmissionRequest, claims map[string]any) (AdmissionClaims, error) { + now := v.now() + if err := validateStringClaim(claims, "iss", v.conf.Issuer); err != nil { + return AdmissionClaims{}, err + } + audience, err := validateAudienceClaim(claims, v.conf.Audience) + if err != nil { + return AdmissionClaims{}, err + } + if err = validateExpiration(claims, now, time.Duration(v.conf.ClockSkewSec)*time.Second); err != nil { + return AdmissionClaims{}, err + } + if err = validateStringClaim(claims, v.conf.NetworkClaim, req.NetworkID); err != nil { + return AdmissionClaims{}, err + } + accountID, err := accountIDFromIdentity(req.Identity) + if err != nil { + return AdmissionClaims{}, ErrAdmissionInvalidToken + } + if err = validateStringClaim(claims, v.conf.IdentityClaim, accountID); err != nil { + return AdmissionClaims{}, err + } + subject, err := stringClaim(claims, v.conf.SubjectClaim) + if err != nil || subject == "" { + return AdmissionClaims{}, ErrAdmissionInvalidToken + } + for name, expected := range v.conf.RequiredClaims { + actual, ok := claims[name] + if !ok || !claimMatches(actual, expected) { + return AdmissionClaims{}, fmt.Errorf("required claim %q is missing or invalid: %w", name, ErrAdmissionInvalidToken) + } + } + return AdmissionClaims{ + Subject: subject, + Issuer: v.conf.Issuer, + Audience: audience, + NetworkID: req.NetworkID, + Identity: req.Identity, + Claims: claims, + }, nil +} + +func accountIDFromIdentity(identity []byte) (string, error) { + pub, err := anycrypto.UnmarshalEd25519PublicKeyProto(identity) + if err != nil { + return "", err + } + return pub.Account(), nil +} + +func validateStringClaim(claims map[string]any, name string, expected string) error { + actual, err := stringClaim(claims, name) + if err != nil || actual != expected { + return ErrAdmissionInvalidToken + } + return nil +} + +func stringClaim(claims map[string]any, name string) (string, error) { + value, ok := claims[name].(string) + if !ok { + return "", ErrAdmissionInvalidToken + } + return value, nil +} + +func validateAudienceClaim(claims map[string]any, expected string) ([]string, error) { + audience := stringListClaim(claims["aud"]) + for _, value := range audience { + if value == expected { + return audience, nil + } + } + return nil, ErrAdmissionInvalidToken +} + +func stringListClaim(value any) []string { + switch value := value.(type) { + case string: + return []string{value} + case []any: + result := make([]string, 0, len(value)) + for _, item := range value { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result + default: + return nil + } +} + +func validateExpiration(claims map[string]any, now time.Time, skew time.Duration) error { + exp, ok, err := numericDateClaim(claims, "exp") + if err != nil || !ok || now.After(exp.Add(skew)) { + return ErrAdmissionInvalidToken + } + if nbf, ok, err := numericDateClaim(claims, "nbf"); err != nil || ok && now.Add(skew).Before(nbf) { + return ErrAdmissionInvalidToken + } + if iat, ok, err := numericDateClaim(claims, "iat"); err != nil || ok && now.Add(skew).Before(iat) { + return ErrAdmissionInvalidToken + } + return nil +} + +func numericDateClaim(claims map[string]any, name string) (time.Time, bool, error) { + value, ok := claims[name] + if !ok { + return time.Time{}, false, nil + } + seconds, err := int64ClaimValue(value) + if err != nil { + return time.Time{}, false, err + } + return time.Unix(seconds, 0), true, nil +} + +func int64ClaimValue(value any) (int64, error) { + switch value := value.(type) { + case json.Number: + return value.Int64() + case float64: + return int64(value), nil + case int64: + return value, nil + case int: + return int64(value), nil + default: + return 0, ErrAdmissionInvalidToken + } +} + +func claimMatches(actual any, expected any) bool { + if values, ok := actual.([]any); ok { + for _, value := range values { + if claimMatches(value, expected) { + return true + } + } + return false + } + switch expected := expected.(type) { + case string: + actual, ok := actual.(string) + return ok && actual == expected + case bool: + actual, ok := actual.(bool) + return ok && actual == expected + case int: + return claimNumberMatches(actual, int64(expected)) + case int64: + return claimNumberMatches(actual, expected) + case json.Number: + expectedInt, err := expected.Int64() + return err == nil && claimNumberMatches(actual, expectedInt) + default: + return actual == expected + } +} + +func claimNumberMatches(actual any, expected int64) bool { + actualInt, err := int64ClaimValue(actual) + return err == nil && actualInt == expected +} diff --git a/net/secureservice/admission_jwt_test.go b/net/secureservice/admission_jwt_test.go new file mode 100644 index 00000000..882d4cbb --- /dev/null +++ b/net/secureservice/admission_jwt_test.go @@ -0,0 +1,227 @@ +package secureservice + +import ( + "context" + stdcrypto "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "math/big" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJWTAdmissionVerifier_VerifyAdmission(t *testing.T) { + now := time.Unix(1700000000, 0) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + account := newTestAccData(t) + identity, err := account.SignKey.GetPublic().Marshall() + require.NoError(t, err) + + verifier := newTestJWTAdmissionVerifier(t, privateKey, now) + token := signAdmissionToken(t, privateKey, map[string]any{ + "iss": "https://issuer.example", + "aud": []string{"any-sync"}, + "sub": "user-1", + "network_id": "network-1", + "anytype_identity": account.SignKey.GetPublic().Account(), + "groups": []string{"docs-users"}, + "exp": now.Add(time.Hour).Unix(), + "nbf": now.Add(-time.Minute).Unix(), + "iat": now.Add(-time.Minute).Unix(), + }) + + decision, err := verifier.VerifyAdmission(context.Background(), AdmissionRequest{ + Token: token, + Identity: identity, + NetworkID: "network-1", + }) + + require.NoError(t, err) + assert.True(t, decision.Allowed) + assert.Equal(t, "admission allowed", decision.Reason) + assert.Equal(t, "user-1", decision.Claims.Subject) + assert.Equal(t, "network-1", decision.Claims.NetworkID) + assert.Equal(t, identity, decision.Claims.Identity) +} + +func TestJWTAdmissionVerifier_DeniesWrongAudience(t *testing.T) { + now := time.Unix(1700000000, 0) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + account := newTestAccData(t) + identity, err := account.SignKey.GetPublic().Marshall() + require.NoError(t, err) + + verifier := newTestJWTAdmissionVerifier(t, privateKey, now) + token := signAdmissionToken(t, privateKey, map[string]any{ + "iss": "https://issuer.example", + "aud": "other-audience", + "sub": "user-1", + "network_id": "network-1", + "anytype_identity": account.SignKey.GetPublic().Account(), + "groups": []string{"docs-users"}, + "exp": now.Add(time.Hour).Unix(), + }) + + decision, err := verifier.VerifyAdmission(context.Background(), AdmissionRequest{ + Token: token, + Identity: identity, + NetworkID: "network-1", + }) + + require.Error(t, err) + assert.True(t, errors.Is(err, ErrAdmissionDenied)) + assert.False(t, decision.Allowed) +} + +func TestJWTAdmissionVerifier_DeniesWrongIdentity(t *testing.T) { + now := time.Unix(1700000000, 0) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + account := newTestAccData(t) + otherAccount := newTestAccData(t) + identity, err := account.SignKey.GetPublic().Marshall() + require.NoError(t, err) + + verifier := newTestJWTAdmissionVerifier(t, privateKey, now) + token := signAdmissionToken(t, privateKey, map[string]any{ + "iss": "https://issuer.example", + "aud": "any-sync", + "sub": "user-1", + "network_id": "network-1", + "anytype_identity": otherAccount.SignKey.GetPublic().Account(), + "groups": []string{"docs-users"}, + "exp": now.Add(time.Hour).Unix(), + }) + + decision, err := verifier.VerifyAdmission(context.Background(), AdmissionRequest{ + Token: token, + Identity: identity, + NetworkID: "network-1", + }) + + require.Error(t, err) + assert.True(t, errors.Is(err, ErrAdmissionDenied)) + assert.False(t, decision.Allowed) +} + +func TestJWTAdmissionVerifier_DeniesTamperedToken(t *testing.T) { + now := time.Unix(1700000000, 0) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + account := newTestAccData(t) + identity, err := account.SignKey.GetPublic().Marshall() + require.NoError(t, err) + + verifier := newTestJWTAdmissionVerifier(t, privateKey, now) + token := signAdmissionToken(t, privateKey, map[string]any{ + "iss": "https://issuer.example", + "aud": "any-sync", + "sub": "user-1", + "network_id": "network-1", + "anytype_identity": account.SignKey.GetPublic().Account(), + "groups": []string{"docs-users"}, + "exp": now.Add(time.Hour).Unix(), + }) + tokenParts := strings.Split(token, ".") + require.Len(t, tokenParts, 3) + tokenParts[2] = base64.RawURLEncoding.EncodeToString([]byte("bad-signature")) + + decision, err := verifier.VerifyAdmission(context.Background(), AdmissionRequest{ + Token: strings.Join(tokenParts, "."), + Identity: identity, + NetworkID: "network-1", + }) + + require.Error(t, err) + assert.True(t, errors.Is(err, ErrAdmissionDenied)) + assert.False(t, decision.Allowed) +} + +func TestJWTAdmissionVerifier_DeniesExpiredToken(t *testing.T) { + now := time.Unix(1700000000, 0) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + account := newTestAccData(t) + identity, err := account.SignKey.GetPublic().Marshall() + require.NoError(t, err) + + verifier := newTestJWTAdmissionVerifier(t, privateKey, now) + token := signAdmissionToken(t, privateKey, map[string]any{ + "iss": "https://issuer.example", + "aud": "any-sync", + "sub": "user-1", + "network_id": "network-1", + "anytype_identity": account.SignKey.GetPublic().Account(), + "groups": []string{"docs-users"}, + "exp": now.Add(-2 * time.Minute).Unix(), + }) + + decision, err := verifier.VerifyAdmission(context.Background(), AdmissionRequest{ + Token: token, + Identity: identity, + NetworkID: "network-1", + }) + + require.Error(t, err) + assert.True(t, errors.Is(err, ErrAdmissionDenied)) + assert.False(t, decision.Allowed) +} + +func newTestJWTAdmissionVerifier(t *testing.T, privateKey *rsa.PrivateKey, now time.Time) *jwtAdmissionVerifier { + verifier, err := newJWTAdmissionVerifier(AdmissionConfig{ + Issuer: "https://issuer.example", + Audience: "any-sync", + RequiredClaims: map[string]any{"groups": "docs-users"}, + }, rsaJWKS(t, &privateKey.PublicKey, "test-key"), func() time.Time { return now }) + require.NoError(t, err) + return verifier +} + +func signAdmissionToken(t *testing.T, privateKey *rsa.PrivateKey, claims map[string]any) string { + header := map[string]any{ + "alg": "RS256", + "kid": "test-key", + } + encodedHeader := encodeJWTPart(t, header) + encodedClaims := encodeJWTPart(t, claims) + signingInput := encodedHeader + "." + encodedClaims + digest := sha256.Sum256([]byte(signingInput)) + signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, stdcrypto.SHA256, digest[:]) + require.NoError(t, err) + return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature) +} + +func rsaJWKS(t *testing.T, publicKey *rsa.PublicKey, keyID string) []byte { + return mustJSON(t, map[string]any{ + "keys": []map[string]any{ + { + "kty": "RSA", + "kid": keyID, + "use": "sig", + "alg": "RS256", + "n": base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()), + "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()), + }, + }, + }) +} + +func encodeJWTPart(t *testing.T, value any) string { + return base64.RawURLEncoding.EncodeToString(mustJSON(t, value)) +} + +func mustJSON(t *testing.T, value any) []byte { + data, err := json.Marshal(value) + require.NoError(t, err) + return data +} From 5610c63e828293b46511a1cc93a54f2eada9c6d0 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Wed, 20 May 2026 15:25:17 +0100 Subject: [PATCH 3/3] feat(secureservice): carry admission tokens Add an additive handshake credentials field for provider-neutral admission tokens and propagate it through secureservice contexts. This only carries tokens for future admission enforcement; it does not validate tokens or change existing handshake behavior. --- net/secureservice/config.go | 39 +++++++++++++++++ net/secureservice/credential.go | 37 +++++++++++++--- net/secureservice/credential_test.go | 25 +++++++++++ .../handshake/credential_test.go | 36 +++++++++++++--- net/secureservice/handshake/handshake.go | 8 ++-- .../handshake/handshakeproto/handshake.pb.go | 27 ++++++++---- .../handshakeproto/handshake_vtproto.pb.go | 43 +++++++++++++++++++ .../handshakeproto/protos/handshake.proto | 3 +- net/secureservice/secureservice.go | 6 ++- net/secureservice/secureservice_test.go | 5 ++- 10 files changed, 202 insertions(+), 27 deletions(-) diff --git a/net/secureservice/config.go b/net/secureservice/config.go index 44a841ce..98a8786b 100644 --- a/net/secureservice/config.go +++ b/net/secureservice/config.go @@ -6,6 +6,8 @@ type ctxKey int const ( allowAccountCheck ctxKey = iota + outboundAdmissionToken + remoteAdmissionToken ) type configGetter interface { @@ -30,3 +32,40 @@ func CtxIsAccountCheckAllowed(ctx context.Context) bool { } return false } + +// CtxWithOutboundAdmissionToken stores the local admission token to send during the handshake. +func CtxWithOutboundAdmissionToken(ctx context.Context, token string) context.Context { + if ctx == nil { + ctx = context.Background() + } + return context.WithValue(ctx, outboundAdmissionToken, token) +} + +// CtxOutboundAdmissionToken returns the local admission token to send during the handshake. +func CtxOutboundAdmissionToken(ctx context.Context) string { + if ctx == nil { + return "" + } + if v, ok := ctx.Value(outboundAdmissionToken).(string); ok { + return v + } + return "" +} + +func ctxWithRemoteAdmissionToken(ctx context.Context, token string) context.Context { + if ctx == nil { + ctx = context.Background() + } + return context.WithValue(ctx, remoteAdmissionToken, token) +} + +// CtxRemoteAdmissionToken returns the remote admission token received during the handshake. +func CtxRemoteAdmissionToken(ctx context.Context) string { + if ctx == nil { + return "" + } + if v, ok := ctx.Value(remoteAdmissionToken).(string); ok { + return v + } + return "" +} diff --git a/net/secureservice/credential.go b/net/secureservice/credential.go index a0051b51..d9f9dbf2 100644 --- a/net/secureservice/credential.go +++ b/net/secureservice/credential.go @@ -43,8 +43,9 @@ func (n noVerifyChecker) CheckCredential(remotePeerId string, cred *handshakepro return } return handshake.Result{ - ProtoVersion: cred.Version, - ClientVersion: cred.ClientVersion, + ProtoVersion: cred.Version, + ClientVersion: cred.ClientVersion, + AdmissionToken: cred.AdmissionToken, }, nil } @@ -117,8 +118,34 @@ func (p *peerSignVerifier) CheckCredential(remotePeerId string, cred *handshakep return } return handshake.Result{ - Identity: msg.Identity, - ProtoVersion: cred.Version, - ClientVersion: cred.ClientVersion, + Identity: msg.Identity, + ProtoVersion: cred.Version, + ClientVersion: cred.ClientVersion, + AdmissionToken: cred.AdmissionToken, }, nil } + +func withAdmissionToken(checker handshake.CredentialChecker, token string) handshake.CredentialChecker { + if token == "" { + return checker + } + return admissionTokenCredentialChecker{ + CredentialChecker: checker, + token: token, + } +} + +type admissionTokenCredentialChecker struct { + handshake.CredentialChecker + token string +} + +func (a admissionTokenCredentialChecker) MakeCredentials(remotePeerId string) *handshakeproto.Credentials { + cred := a.CredentialChecker.MakeCredentials(remotePeerId) + if cred == nil { + return nil + } + withToken := *cred + withToken.AdmissionToken = a.token + return &withToken +} diff --git a/net/secureservice/credential_test.go b/net/secureservice/credential_test.go index 94e24f19..596fe3d7 100644 --- a/net/secureservice/credential_test.go +++ b/net/secureservice/credential_test.go @@ -25,18 +25,43 @@ func TestPeerSignVerifier_CheckCredential(t *testing.T) { cr1 := cc1.MakeCredentials(c1) cr2 := cc2.MakeCredentials(c2) + cr1.AdmissionToken = "token-1" + cr2.AdmissionToken = "token-2" res, err := cc1.CheckCredential(c1, cr2) assert.NoError(t, err) assert.Equal(t, identity2, res.Identity) + assert.Equal(t, "token-2", res.AdmissionToken) res2, err := cc2.CheckCredential(c2, cr1) assert.NoError(t, err) assert.Equal(t, identity1, res2.Identity) + assert.Equal(t, "token-1", res2.AdmissionToken) _, err = cc1.CheckCredential(c1, cr1) assert.EqualError(t, err, handshake.ErrInvalidCredentials.Error()) } +func TestNoVerifyChecker_CheckCredentialCopiesAdmissionToken(t *testing.T) { + cc := newNoVerifyChecker(1, []uint32{1}, "test:v1") + cred := cc.MakeCredentials("peer-id") + cred.AdmissionToken = "admission-token" + + res, err := cc.CheckCredential("peer-id", cred) + require.NoError(t, err) + assert.Equal(t, "admission-token", res.AdmissionToken) +} + +func TestAdmissionTokenCheckerDoesNotMutateBaseCredentials(t *testing.T) { + cc := newNoVerifyChecker(1, []uint32{1}, "test:v1") + wrapped := withAdmissionToken(cc, "admission-token") + + wrappedCred := wrapped.MakeCredentials("peer-id") + baseCred := cc.MakeCredentials("peer-id") + + assert.Equal(t, "admission-token", wrappedCred.AdmissionToken) + assert.Empty(t, baseCred.AdmissionToken) +} + func TestIncompatibleVersion(t *testing.T) { a1 := newTestAccData(t) a2 := newTestAccData(t) diff --git a/net/secureservice/handshake/credential_test.go b/net/secureservice/handshake/credential_test.go index fe6687a6..741cd763 100644 --- a/net/secureservice/handshake/credential_test.go +++ b/net/secureservice/handshake/credential_test.go @@ -18,9 +18,10 @@ var noVerifyChecker = &testCredChecker{ makeCred: &handshakeproto.Credentials{Type: handshakeproto.CredentialsType_SkipVerify, ClientVersion: "test:v1.0"}, checkCred: func(peerId string, cred *handshakeproto.Credentials) (res Result, err error) { return Result{ - Identity: []byte("identity"), - ProtoVersion: cred.Version, - ClientVersion: cred.ClientVersion, + Identity: []byte("identity"), + ProtoVersion: cred.Version, + ClientVersion: cred.ClientVersion, + AdmissionToken: cred.AdmissionToken, }, nil }, } @@ -46,7 +47,10 @@ func TestOutgoingHandshake(t *testing.T) { _, err = noVerifyChecker.CheckCredential("p1", msg.cred) require.NoError(t, err) // send credential message - require.NoError(t, h.writeCredentials(noVerifyChecker.MakeCredentials(""))) + const remoteToken = "remote-admission-token" + remoteCred := *noVerifyChecker.MakeCredentials("") + remoteCred.AdmissionToken = remoteToken + require.NoError(t, h.writeCredentials(&remoteCred)) // receive ack msg, err = h.readMsg(msgTypeAck) require.NoError(t, err) @@ -55,6 +59,7 @@ func TestOutgoingHandshake(t *testing.T) { require.NoError(t, h.writeAck(handshakeproto.Error_Null)) res := <-handshakeResCh assert.NotEmpty(t, res.res) + assert.Equal(t, remoteToken, res.res.AdmissionToken) assert.NoError(t, res.err) }) t.Run("write cred err", func(t *testing.T) { @@ -233,6 +238,7 @@ func TestOutgoingHandshake(t *testing.T) { func TestIncomingHandshake(t *testing.T) { t.Run("success", func(t *testing.T) { c1, c2 := newConnPair(t) + const remoteToken = "remote-admission-token" var handshakeResCh = make(chan handshakeRes, 1) go func() { identity, err := IncomingHandshake(nil, c1, "", noVerifyChecker) @@ -241,7 +247,9 @@ func TestIncomingHandshake(t *testing.T) { h := newHandshake() h.conn = c2 // write credentials - require.NoError(t, h.writeCredentials(noVerifyChecker.MakeCredentials(""))) + remoteCred := *noVerifyChecker.MakeCredentials("") + remoteCred.AdmissionToken = remoteToken + require.NoError(t, h.writeCredentials(&remoteCred)) // wait credentials msg, err := h.readMsg(msgTypeCred) require.NoError(t, err) @@ -255,6 +263,7 @@ func TestIncomingHandshake(t *testing.T) { assert.Equal(t, handshakeproto.Error_Null, msg.ack.Error) res := <-handshakeResCh assert.NotEmpty(t, res.res) + assert.Equal(t, remoteToken, res.res.AdmissionToken) require.NoError(t, res.err) }) t.Run("write cred err", func(t *testing.T) { @@ -490,26 +499,39 @@ func TestEndToEnd(t *testing.T) { inResCh = make(chan handshakeRes, 1) outResCh = make(chan handshakeRes, 1) ) + clientChecker := cloneNoVerifyCheckerWithAdmissionToken("client-admission-token") + serverChecker := cloneNoVerifyCheckerWithAdmissionToken("server-admission-token") st := time.Now() go func() { - identity, err := OutgoingHandshake(nil, c1, "", noVerifyChecker) + identity, err := OutgoingHandshake(nil, c1, "", clientChecker) outResCh <- handshakeRes{res: identity, err: err} }() go func() { - identity, err := IncomingHandshake(nil, c2, "", noVerifyChecker) + identity, err := IncomingHandshake(nil, c2, "", serverChecker) inResCh <- handshakeRes{res: identity, err: err} }() outRes := <-outResCh assert.NoError(t, outRes.err) assert.NotEmpty(t, outRes.res) + assert.Equal(t, "server-admission-token", outRes.res.AdmissionToken) inRes := <-inResCh assert.NoError(t, inRes.err) assert.NotEmpty(t, inRes.res) + assert.Equal(t, "client-admission-token", inRes.res.AdmissionToken) t.Log("dur", time.Since(st)) } +func cloneNoVerifyCheckerWithAdmissionToken(token string) *testCredChecker { + cred := *noVerifyChecker.makeCred + cred.AdmissionToken = token + return &testCredChecker{ + makeCred: &cred, + checkCred: noVerifyChecker.checkCred, + } +} + func BenchmarkHandshake(b *testing.B) { c1, c2 := newConnPair(b) var ( diff --git a/net/secureservice/handshake/handshake.go b/net/secureservice/handshake/handshake.go index 845c8de7..5ec62fbf 100644 --- a/net/secureservice/handshake/handshake.go +++ b/net/secureservice/handshake/handshake.go @@ -70,9 +70,10 @@ type CredentialChecker interface { } type Result struct { - Identity []byte - ProtoVersion uint32 - ClientVersion string + Identity []byte + ProtoVersion uint32 + ClientVersion string + AdmissionToken string } func newHandshake() *handshake { @@ -191,6 +192,7 @@ func (h *handshake) release() { h.remoteAck.Error = 0 h.remoteCred.Type = 0 h.remoteCred.Payload = h.remoteCred.Payload[:0] + h.remoteCred.AdmissionToken = "" h.remoteProto.Proto = 0 h.remoteProto.Encodings = h.remoteProto.Encodings[:0] handshakePool.Put(h) diff --git a/net/secureservice/handshake/handshakeproto/handshake.pb.go b/net/secureservice/handshake/handshakeproto/handshake.pb.go index c0322e8b..8df89faf 100644 --- a/net/secureservice/handshake/handshakeproto/handshake.pb.go +++ b/net/secureservice/handshake/handshakeproto/handshake.pb.go @@ -223,13 +223,14 @@ func (Encoding) EnumDescriptor() ([]byte, []int) { } type Credentials struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type CredentialsType `protobuf:"varint,1,opt,name=type,proto3,enum=anyHandshake.CredentialsType" json:"type,omitempty"` - Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` - Version uint32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` - ClientVersion string `protobuf:"bytes,4,opt,name=clientVersion,proto3" json:"clientVersion,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Type CredentialsType `protobuf:"varint,1,opt,name=type,proto3,enum=anyHandshake.CredentialsType" json:"type,omitempty"` + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` + Version uint32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + ClientVersion string `protobuf:"bytes,4,opt,name=clientVersion,proto3" json:"clientVersion,omitempty"` + AdmissionToken string `protobuf:"bytes,5,opt,name=admissionToken,proto3" json:"admissionToken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Credentials) Reset() { @@ -290,6 +291,13 @@ func (x *Credentials) GetClientVersion() string { return "" } +func (x *Credentials) GetAdmissionToken() string { + if x != nil { + return x.AdmissionToken + } + return "" +} + type PayloadSignedPeerIds struct { state protoimpl.MessageState `protogen:"open.v1"` // account identity @@ -444,12 +452,13 @@ var File_net_secureservice_handshake_handshakeproto_protos_handshake_proto proto const file_net_secureservice_handshake_handshakeproto_protos_handshake_proto_rawDesc = "" + "\n" + - "Anet/secureservice/handshake/handshakeproto/protos/handshake.proto\x12\fanyHandshake\"\x9a\x01\n" + + "Anet/secureservice/handshake/handshakeproto/protos/handshake.proto\x12\fanyHandshake\"\xc2\x01\n" + "\vCredentials\x121\n" + "\x04type\x18\x01 \x01(\x0e2\x1d.anyHandshake.CredentialsTypeR\x04type\x12\x18\n" + "\apayload\x18\x02 \x01(\fR\apayload\x12\x18\n" + "\aversion\x18\x03 \x01(\rR\aversion\x12$\n" + - "\rclientVersion\x18\x04 \x01(\tR\rclientVersion\"F\n" + + "\rclientVersion\x18\x04 \x01(\tR\rclientVersion\x12&\n" + + "\x0eadmissionToken\x18\x05 \x01(\tR\x0eadmissionToken\"F\n" + "\x14PayloadSignedPeerIds\x12\x1a\n" + "\bidentity\x18\x01 \x01(\fR\bidentity\x12\x12\n" + "\x04sign\x18\x02 \x01(\fR\x04sign\"0\n" + diff --git a/net/secureservice/handshake/handshakeproto/handshake_vtproto.pb.go b/net/secureservice/handshake/handshakeproto/handshake_vtproto.pb.go index 55a1fe9c..51ad5cbb 100644 --- a/net/secureservice/handshake/handshakeproto/handshake_vtproto.pb.go +++ b/net/secureservice/handshake/handshakeproto/handshake_vtproto.pb.go @@ -48,6 +48,13 @@ func (m *Credentials) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.AdmissionToken) > 0 { + i -= len(m.AdmissionToken) + copy(dAtA[i:], m.AdmissionToken) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.AdmissionToken))) + i-- + dAtA[i] = 0x2a + } if len(m.ClientVersion) > 0 { i -= len(m.ClientVersion) copy(dAtA[i:], m.ClientVersion) @@ -239,6 +246,10 @@ func (m *Credentials) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + l = len(m.AdmissionToken) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -427,6 +438,38 @@ func (m *Credentials) UnmarshalVT(dAtA []byte) error { } m.ClientVersion = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AdmissionToken", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AdmissionToken = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/net/secureservice/handshake/handshakeproto/protos/handshake.proto b/net/secureservice/handshake/handshakeproto/protos/handshake.proto index 624a1351..6fe435d5 100644 --- a/net/secureservice/handshake/handshakeproto/protos/handshake.proto +++ b/net/secureservice/handshake/handshakeproto/protos/handshake.proto @@ -40,6 +40,7 @@ message Credentials { bytes payload = 2; uint32 version = 3; string clientVersion = 4; + string admissionToken = 5; } enum CredentialsType { @@ -93,4 +94,4 @@ enum ProtoType { enum Encoding { None = 0; Snappy = 1; -} \ No newline at end of file +} diff --git a/net/secureservice/secureservice.go b/net/secureservice/secureservice.go index ab85933d..8a0a3f4a 100644 --- a/net/secureservice/secureservice.go +++ b/net/secureservice/secureservice.go @@ -134,7 +134,8 @@ func (s *secureService) SecureInbound(ctx context.Context, conn net.Conn) (cctx } func (s *secureService) HandshakeInbound(ctx context.Context, conn io.ReadWriteCloser, peerId string) (cctx context.Context, err error) { - res, err := handshake.IncomingHandshake(ctx, conn, peerId, s.inboundChecker) + checker := withAdmissionToken(s.inboundChecker, CtxOutboundAdmissionToken(ctx)) + res, err := handshake.IncomingHandshake(ctx, conn, peerId, checker) if err != nil { return nil, err } @@ -143,6 +144,7 @@ func (s *secureService) HandshakeInbound(ctx context.Context, conn io.ReadWriteC cctx = peer.CtxWithIdentity(cctx, res.Identity) cctx = peer.CtxWithClientVersion(cctx, res.ClientVersion) cctx = peer.CtxWithProtoVersion(cctx, res.ProtoVersion) + cctx = ctxWithRemoteAdmissionToken(cctx, res.AdmissionToken) return } @@ -162,6 +164,7 @@ func (s *secureService) HandshakeOutbound(ctx context.Context, conn io.ReadWrite } else { checker = s.noVerifyChecker } + checker = withAdmissionToken(checker, CtxOutboundAdmissionToken(ctx)) res, err := handshake.OutgoingHandshake(ctx, conn, peerId, checker) if err != nil { return nil, err @@ -171,6 +174,7 @@ func (s *secureService) HandshakeOutbound(ctx context.Context, conn io.ReadWrite cctx = peer.CtxWithIdentity(cctx, res.Identity) cctx = peer.CtxWithClientVersion(cctx, res.ClientVersion) cctx = peer.CtxWithProtoVersion(cctx, res.ProtoVersion) + cctx = ctxWithRemoteAdmissionToken(cctx, res.AdmissionToken) return cctx, nil } diff --git a/net/secureservice/secureservice_test.go b/net/secureservice/secureservice_test.go index bc4dea2d..5cee5f59 100644 --- a/net/secureservice/secureservice_test.go +++ b/net/secureservice/secureservice_test.go @@ -39,7 +39,8 @@ func TestHandshake(t *testing.T) { fxC := newFixture(t, nc, nc.GetAccountService(1), 1, []uint32{1}) defer fxC.Finish(t) - cctx, err := fxC.SecureOutbound(ctx, cc) + const admissionToken = "client-admission-token" + cctx, err := fxC.SecureOutbound(CtxWithOutboundAdmissionToken(ctx, admissionToken), cc) require.NoError(t, err) ctxPeerId, err := peer.CtxPeerId(cctx) require.NoError(t, err) @@ -53,6 +54,8 @@ func TestHandshake(t *testing.T) { marshalledId, _ := nc.GetAccountService(1).Account().SignKey.GetPublic().Marshall() assert.Equal(t, nc.GetAccountService(1).Account().PeerId, peerId) assert.Equal(t, marshalledId, accId) + assert.Equal(t, admissionToken, CtxRemoteAdmissionToken(res.ctx)) + assert.Empty(t, CtxOutboundAdmissionToken(res.ctx)) } func TestHandshakeIncompatibleVersion(t *testing.T) {