mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 20:06:37 +00:00
feat(connect): add device identity store and registration proof (#6267)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
3d602080f7ca4c32ba9e37ad1a32665c78560726b30aeee08fd9e95eb2f36194 accept-vectors.json
|
||||
d3c19946288717088145592e0e8d6f2fa684443ba2f73d4c7bc49c415d6dd051 certificate-profile.json
|
||||
060485263c51003274c056a0e04bec1b7d76157cf599ba79eebe040bc7cee71b error-codes.json
|
||||
43fe297ffb512b1b9f4af62f1832f3aa3905157893bfdc3dcc6d56f5a98aaef6 reject-vectors.json
|
||||
b946175b094f4a8d75091b652fbe3d4327c9c795f28e02c96e1ab90a429e418d surface-separation.json
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "auth",
|
||||
"fixture": "accept-vectors",
|
||||
"description": "Presented certificates that authenticate. Time offsets are seconds relative to the moment the request is evaluated.",
|
||||
"vectors": [
|
||||
{
|
||||
"name": "current credential on an active cluster",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": true,
|
||||
"reason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "first device authenticates while its cluster is still pending",
|
||||
"clusterState": "PENDING",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -60,
|
||||
"validUntilOffsetSeconds": 86340,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": true,
|
||||
"reason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "outgoing credential inside the bounded rotation overlap",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": "rotate"
|
||||
},
|
||||
"presented": {
|
||||
"credential": "outgoing",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": true,
|
||||
"reason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "incoming credential immediately after rotation",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": "rotate"
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": true,
|
||||
"reason": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "auth",
|
||||
"fixture": "certificate-profile",
|
||||
"description": "Frozen shape of the client certificate an online device presents and of the RFC 9440 header that conveys it.",
|
||||
"certificate": {
|
||||
"subject": {
|
||||
"rdnCount": 1,
|
||||
"commonName": "{clusterDeviceUid}",
|
||||
"forbiddenAttributes": ["O", "OU", "C", "ST", "L", "emailAddress"]
|
||||
},
|
||||
"subjectAlternativeName": {
|
||||
"entryCount": 1,
|
||||
"type": "uniformResourceIdentifier",
|
||||
"value": "urn:rustfs:connect:device:{clusterDeviceUid}",
|
||||
"forbiddenTypes": ["dNSName", "iPAddress", "rfc822Name", "directoryName"],
|
||||
"wildcardsAccepted": false
|
||||
},
|
||||
"clusterDeviceUid": {
|
||||
"source": "cluster_devices.uid",
|
||||
"format": "lowercase canonical UUIDv7",
|
||||
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
},
|
||||
"keyAlgorithm": "EC",
|
||||
"keyCurve": "P-256",
|
||||
"signatureAlgorithm": "ES256",
|
||||
"certificateSigningRequest": {
|
||||
"format": "PKCS#10",
|
||||
"signatureAlgorithm": "ES256",
|
||||
"proofOfPossession": "self-signed with the device private key"
|
||||
},
|
||||
"lifetimeSeconds": 86400,
|
||||
"maxRotationOverlapSeconds": 86400,
|
||||
"recommendedRotationLeadSeconds": 28800,
|
||||
"maxPresentableCredentialsPerDevice": 2,
|
||||
"serial": {
|
||||
"encoding": "lowercase-hex",
|
||||
"length": 32,
|
||||
"pattern": "^[0-9a-f]{32}$",
|
||||
"entropyBits": 128
|
||||
},
|
||||
"certificateFingerprint": {
|
||||
"algorithm": "SHA-256",
|
||||
"over": "DER certificate",
|
||||
"encoding": "lowercase-hex",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"publicKeyFingerprint": {
|
||||
"algorithm": "SHA-256",
|
||||
"over": "DER SubjectPublicKeyInfo",
|
||||
"encoding": "lowercase-hex",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"keyId": {
|
||||
"pattern": "^[a-z0-9][a-z0-9._-]{7,127}$"
|
||||
},
|
||||
"carriesOrganizationIdentifier": false,
|
||||
"carriesClusterIdentifier": false,
|
||||
"tenantBinding": {
|
||||
"source": "device_credentials matched by certificate serial and certificate fingerprint",
|
||||
"resolver": "ClusterDeviceIdentityPort::resolveOnlineCertificate"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"name": "Client-Cert",
|
||||
"specification": "RFC 9440",
|
||||
"encoding": "sf-binary",
|
||||
"valueTemplate": ":{base64(DER certificate)}:",
|
||||
"example": ":MIIBkDCCATagAwIBAgIQZXhhbXBsZQ==:",
|
||||
"chainHeader": {
|
||||
"name": "Client-Cert-Chain",
|
||||
"accepted": false,
|
||||
"reason": "Chain validation belongs to the trusted ingress, which verifies against the Connect device CA before forwarding."
|
||||
},
|
||||
"setByTrustedIngressOnly": true,
|
||||
"inboundHeaderStripped": true,
|
||||
"appendAccepted": false,
|
||||
"acceptedOnSurfaces": ["/agent"],
|
||||
"ignoredOnSurfaces": ["/api"],
|
||||
"backendPubliclyReachable": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "auth",
|
||||
"fixture": "error-codes",
|
||||
"description": "Frozen ErrorInfo reasons for agent authentication and negotiation. Clients branch on status and reason, never on message.",
|
||||
"domain": "rustfs.connect",
|
||||
"detailType": "type.googleapis.com/google.rpc.ErrorInfo",
|
||||
"disclosureRules": [
|
||||
"A rejection never reveals whether an unknown certificate belongs to another tenant.",
|
||||
"A rejection never contains certificate bytes, key material, or a fingerprint."
|
||||
],
|
||||
"reasons": [
|
||||
{
|
||||
"reason": "UNSUPPORTED_PROTOCOL",
|
||||
"httpStatus": 400,
|
||||
"status": "INVALID_ARGUMENT",
|
||||
"meaning": "The requested protocol major version is missing, malformed, or not supported."
|
||||
},
|
||||
{
|
||||
"reason": "CLIENT_CERTIFICATE_MISSING",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "The request reached an authenticated agent operation without a Client-Cert header from trusted ingress."
|
||||
},
|
||||
{
|
||||
"reason": "CLIENT_CERTIFICATE_MALFORMED",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "The Client-Cert header is not a valid RFC 9440 byte sequence, or the certificate violates the frozen profile."
|
||||
},
|
||||
{
|
||||
"reason": "CLIENT_CERTIFICATE_UNKNOWN",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "No device credential matches the presented certificate serial and fingerprint together."
|
||||
},
|
||||
{
|
||||
"reason": "CREDENTIAL_NOT_YET_VALID",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "The matched credential's validity window has not opened yet."
|
||||
},
|
||||
{
|
||||
"reason": "CREDENTIAL_EXPIRED",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "The matched credential's validity window has closed, ending any rotation overlap."
|
||||
},
|
||||
{
|
||||
"reason": "CREDENTIAL_REVOKED",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "The matched credential is REVOKED. Revocation takes effect immediately."
|
||||
},
|
||||
{
|
||||
"reason": "CREDENTIAL_COMPROMISED",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "The matched credential is COMPROMISED, for example after a clone was observed."
|
||||
},
|
||||
{
|
||||
"reason": "CLUSTER_DISABLED",
|
||||
"httpStatus": 403,
|
||||
"status": "PERMISSION_DENIED",
|
||||
"meaning": "The credential is intact but its cluster is DISABLED, so no agent activity is accepted."
|
||||
},
|
||||
{
|
||||
"reason": "CLUSTER_DELETED",
|
||||
"httpStatus": 403,
|
||||
"status": "PERMISSION_DENIED",
|
||||
"meaning": "The credential is intact but its cluster is DELETED."
|
||||
},
|
||||
{
|
||||
"reason": "TENANT_MISMATCH",
|
||||
"httpStatus": 403,
|
||||
"status": "PERMISSION_DENIED",
|
||||
"meaning": "The authenticated device belongs to a different organization than the resource named by the request."
|
||||
},
|
||||
{
|
||||
"reason": "SESSION_CREDENTIAL_NOT_ACCEPTED",
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"meaning": "A browser session cookie was presented to an authenticated agent operation. The agent surface never accepts it."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "auth",
|
||||
"fixture": "reject-vectors",
|
||||
"description": "Presented certificates that must not authenticate. A credential that is known but unusable still resolves, so the rejection can be audited against a device instead of being reported as an unknown certificate.",
|
||||
"vectors": [
|
||||
{
|
||||
"name": "revoked credential",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": "revoke"
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CREDENTIAL_REVOKED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "compromised credential",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": "markCompromised"
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CREDENTIAL_COMPROMISED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "credential whose validity window has closed",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -86460,
|
||||
"validUntilOffsetSeconds": -60,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CREDENTIAL_EXPIRED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "credential whose validity window has not opened",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": 3600,
|
||||
"validUntilOffsetSeconds": 90000,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CREDENTIAL_NOT_YET_VALID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "intact credential on a disabled cluster",
|
||||
"clusterState": "DISABLED",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CLUSTER_DISABLED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "intact credential on a deleted cluster",
|
||||
"clusterState": "DELETED",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CLUSTER_DELETED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "certificate Connect never issued",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "unrelated",
|
||||
"certificateFingerprint": "unrelated"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": false,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CLIENT_CERTIFICATE_UNKNOWN"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issued serial presented with a substituted certificate",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "matching",
|
||||
"certificateFingerprint": "unrelated"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": false,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CLIENT_CERTIFICATE_UNKNOWN"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issued certificate presented under a substituted serial",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"presented": {
|
||||
"credential": "current",
|
||||
"serial": "unrelated",
|
||||
"certificateFingerprint": "matching"
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": false,
|
||||
"authenticationEffective": false,
|
||||
"reason": "CLIENT_CERTIFICATE_UNKNOWN"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tenantVector": {
|
||||
"name": "authenticated device reaching a resource owned by another organization",
|
||||
"description": "The certificate is valid and its credential is effective. Only the stored organization decides what the device may reach, and the certificate carries no organization identifier to contradict it.",
|
||||
"clusterState": "ACTIVE",
|
||||
"credential": {
|
||||
"validFromOffsetSeconds": -3600,
|
||||
"validUntilOffsetSeconds": 82800,
|
||||
"transition": null
|
||||
},
|
||||
"expected": {
|
||||
"credentialResolved": true,
|
||||
"authenticationEffective": true,
|
||||
"resolvedIdentityBelongsToForeignOrganization": false,
|
||||
"reason": "TENANT_MISMATCH"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"protocolVersion": "v1",
|
||||
"fixtureSet": "auth",
|
||||
"fixture": "surface-separation",
|
||||
"description": "The control surface and the agent surface have disjoint credentials. Neither accepts the other's, and neither OpenAPI document declares the other's security scheme.",
|
||||
"httpVectors": [
|
||||
{
|
||||
"name": "agent client certificate presented to the control surface",
|
||||
"surface": "/api",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"path": "/api/session",
|
||||
"headers": {
|
||||
"Client-Cert": ":MIIBkDCCATagAwIBAgIQZXhhbXBsZQ==:"
|
||||
},
|
||||
"browserSession": false
|
||||
},
|
||||
"expected": {
|
||||
"authenticated": false,
|
||||
"httpStatus": 401,
|
||||
"status": "UNAUTHENTICATED",
|
||||
"reason": "UNAUTHENTICATED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "browser session presented to the agent surface",
|
||||
"surface": "/agent",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"path": "/agent/protocolStatus",
|
||||
"headers": {},
|
||||
"browserSession": true
|
||||
},
|
||||
"expected": {
|
||||
"deviceIdentityEstablished": false,
|
||||
"httpStatus": 200,
|
||||
"body": {
|
||||
"protocolVersion": "v1"
|
||||
},
|
||||
"identicalToUnauthenticatedRequest": true,
|
||||
"note": "getProtocolStatus is the pre-registration operation and is public on purpose. An authenticated browser session neither changes its answer nor grants anything on the agent surface."
|
||||
}
|
||||
}
|
||||
],
|
||||
"documentVectors": [
|
||||
{
|
||||
"document": "openapi/agent.json",
|
||||
"securityScheme": "agentMutualTls",
|
||||
"schemeType": "mutualTLS",
|
||||
"forbiddenSecuritySchemes": ["sessionCookie"],
|
||||
"defaultSecurity": ["agentMutualTls"],
|
||||
"publicOperations": ["getProtocolStatus"]
|
||||
},
|
||||
{
|
||||
"document": "openapi/control.json",
|
||||
"securityScheme": "sessionCookie",
|
||||
"schemeType": "apiKey",
|
||||
"forbiddenSecuritySchemes": ["agentMutualTls"],
|
||||
"defaultSecurity": [],
|
||||
"publicOperations": null
|
||||
}
|
||||
],
|
||||
"routeGuards": {
|
||||
"surfacePrefix": "agent/",
|
||||
"description": "No agent route may be protected by a session authentication guard. A device is identified by its certificate or not at all.",
|
||||
"forbiddenMiddlewarePrefixes": ["auth:", "auth.session"],
|
||||
"forbiddenMiddleware": ["auth"],
|
||||
"forbiddenMiddlewareClasses": [
|
||||
"Illuminate\\Auth\\Middleware\\Authenticate",
|
||||
"Illuminate\\Auth\\Middleware\\AuthenticateSession"
|
||||
],
|
||||
"knownGap": {
|
||||
"middleware": "Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful",
|
||||
"description": "The agent routes still share the api middleware group with the control surface, so Sanctum's stateful frontend middleware runs on them. It establishes no device identity and no agent route uses an authentication guard, but the agent surface should get its own middleware group when the first authenticated agent operation lands.",
|
||||
"owner": "the issue that adds the first authenticated agent operation"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user