001package gudusoft.gsqlparser.runtime; 002 003import gudusoft.gsqlparser.util.Logger; 004import gudusoft.gsqlparser.util.LoggerFactory; 005 006import java.security.SecureRandom; 007 008/** 009 * Entry point for activating this GSP runtime with a signed entitlement. 010 * 011 * <p>Replaces the former {@code TSQLEnv.usedBySqlflow} guard, which was a 012 * private boolean a host set by reflection: anyone who read the bytecode knew 013 * the secret, and the failure mode for everyone else was silently wrong 014 * results. This is a signed, expiring, revocable, auditable grant instead.</p> 015 * 016 * <h3>Usage</h3> 017 * <pre> 018 * GspActivation result = GspRuntime.activate(Files.readAllBytes(path)); 019 * if (!result.isValid()) { 020 * throw new IllegalStateException("GSP entitlement rejected: " 021 * + result.reasonCode() + " audit=" + result.auditId()); 022 * } 023 * </pre> 024 * 025 * <p>Activate once per process, before the service reports ready, and treat 026 * failure as a startup failure rather than a per-request one.</p> 027 * 028 * <p><b>There is deliberately no {@code setActivated(true)}.</b> The only way 029 * to reach {@link GspActivationStatus#ACTIVE} is to present bytes carrying a 030 * valid signature from a private key this build does not contain.</p> 031 * 032 * <h3>What this build does and does not do</h3> 033 * 034 * <p>{@code activate} performs full verification and reports an accurate 035 * verdict. <b>Nothing yet refuses to produce results when the runtime is not 036 * activated</b> — wiring enforcement into the result-producing entry points is 037 * the next step. Until then this API is observable but not enforcing, and 038 * saying otherwise would overstate what the jar protects.</p> 039 */ 040public final class GspRuntime { 041 042 private static final Logger log = LoggerFactory.getLogger(GspRuntime.class); 043 044 private static final SecureRandom AUDIT_IDS = new SecureRandom(); 045 046 /** 047 * Current activation. Volatile because activation happens on a startup 048 * thread while reads come from parsing threads. 049 * 050 * <p>Never null: before any attempt it reports 051 * {@link GspActivationStatus#MISSING}, so a caller that skipped activation 052 * and one that failed it are both plainly not activated.</p> 053 */ 054 private static volatile GspActivation current = 055 new GspActivation(GspActivationStatus.MISSING, null, null, 0L); 056 057 private GspRuntime() { 058 // static entry point only 059 } 060 061 /** 062 * Verifies a signed entitlement and records the outcome. 063 * 064 * <p>Every refusal is logged at ERROR with a greppable marker and an audit 065 * id matching the returned object. That pairing is a condition of the 066 * agreed design: a covert refusal is only acceptable when an operator can 067 * still find out why from the host's own logs.</p> 068 * 069 * @param signedEntitlement compact JWS bytes; null or empty yields 070 * {@link GspActivationStatus#MISSING} 071 * @return the outcome; never null, never throws 072 */ 073 public static GspActivation activate(byte[] signedEntitlement) { 074 String auditId = newAuditId(); 075 GspActivation result; 076 try { 077 result = EntitlementVerifier.verify( 078 signedEntitlement, System.currentTimeMillis() / 1000L, auditId); 079 } catch (Throwable t) { 080 // Verification must never propagate. An exception escaping here 081 // would be a denial-of-service on the host's startup path, and 082 // worse, a caller catching broadly could mistake it for "no 083 // entitlement configured" and continue. 084 log.error("GSP_ENTITLEMENT_REFUSED reason=MALFORMED audit=" + auditId 085 + " (verifier raised " + t.getClass().getName() + ")", t); 086 result = new GspActivation(GspActivationStatus.MALFORMED, auditId, null, 0L); 087 } 088 089 if (result.isValid()) { 090 log.info("GSP_ENTITLEMENT_ACCEPTED audit=" + result.auditId() 091 + " deployment=" + result.deploymentId() 092 + " expiresAt=" + result.expiresAtEpochSeconds()); 093 } else { 094 log.error("GSP_ENTITLEMENT_REFUSED reason=" + result.reasonCode() 095 + " audit=" + result.auditId()); 096 } 097 current = result; 098 return result; 099 } 100 101 /** 102 * Whether this runtime currently holds a valid entitlement. 103 * 104 * <p>Re-checks expiry on every call, so a long-running process does not 105 * stay activated past {@code exp} just because it started before it.</p> 106 */ 107 public static boolean isActivated() { 108 return activationStatus().isValid(); 109 } 110 111 /** 112 * The current activation, re-evaluated for expiry. 113 * 114 * @return never null; {@link GspActivationStatus#MISSING} before any 115 * {@link #activate} call 116 */ 117 public static GspActivation activationStatus() { 118 GspActivation snapshot = current; 119 if (snapshot.isValid() 120 && snapshot.expiresAtEpochSeconds() > 0L 121 && System.currentTimeMillis() / 1000L > snapshot.expiresAtEpochSeconds()) { 122 GspActivation expired = new GspActivation( 123 GspActivationStatus.EXPIRED, snapshot.auditId(), 124 snapshot.deploymentId(), snapshot.expiresAtEpochSeconds()); 125 log.error("GSP_ENTITLEMENT_REFUSED reason=EXPIRED audit=" + expired.auditId() 126 + " (entitlement expired while the process was running)"); 127 current = expired; 128 return expired; 129 } 130 return snapshot; 131 } 132 133 /** 134 * Marker resource that makes this build refuse to work without an 135 * entitlement. Added by the SQLFlow build profile; absent from the ordinary 136 * commercial and trial jars, which must keep working for every existing 137 * customer exactly as before. 138 */ 139 private static final String ENFORCEMENT_MARKER = "META-INF/gsp-entitlement-required"; 140 141 /** Resolved once. {@code Boolean} rather than boolean so tests can override. */ 142 private static volatile Boolean enforcementOverride = null; 143 144 private static final boolean MARKER_PRESENT = 145 GspRuntime.class.getClassLoader().getResource(ENFORCEMENT_MARKER) != null; 146 147 /** 148 * Whether this build refuses to produce results without an entitlement. 149 * 150 * <p>False for the ordinary jars, so this whole mechanism is invisible to 151 * existing users. True only for a build that shipped the marker.</p> 152 * 153 * <p>Honest about its limits: a marker on the classpath raises the cost of 154 * removing the check, it is not a security boundary. An attacker who 155 * controls the JVM can strip it, just as they could patch the check out of 156 * the bytecode. Obfuscation and this marker buy cost, not impossibility.</p> 157 */ 158 public static boolean isEnforcementEnabled() { 159 Boolean override = enforcementOverride; 160 return override != null ? override.booleanValue() : MARKER_PRESENT; 161 } 162 163 /** 164 * Refuses to continue when an entitlement-gated build is not activated. 165 * 166 * <p>Called from the result-producing entry points. On a build without the 167 * marker this is a cheap no-op.</p> 168 * 169 * @throws GspNotActivatedException explicitly, rather than degrading the 170 * result — see that class for why covert failure was rejected 171 */ 172 public static void requireActivated() { 173 if (!isEnforcementEnabled()) { 174 return; 175 } 176 GspActivation state = activationStatus(); 177 if (!state.isValid()) { 178 if (state.auditId() == null) { 179 // The never-activated state is a constant and carries no audit 180 // id, so a refusal here would log "audit=null" -- useless to 181 // the operator who has to explain a failed startup. The agreed 182 // design makes the audit id a CONDITION of refusing at all, so 183 // mint one at the refusal point rather than log a null. 184 state = new GspActivation(state.getStatus(), newAuditId(), 185 state.deploymentId(), state.expiresAtEpochSeconds()); 186 } 187 log.error("GSP_ENTITLEMENT_REFUSED reason=" + state.reasonCode() 188 + " audit=" + state.auditId() 189 + " (refusing to produce a result on an entitlement-gated build)"); 190 throw new GspNotActivatedException(state); 191 } 192 } 193 194 /** Test seam: force enforcement on or off; null restores the build default. */ 195 static void setEnforcementForTesting(Boolean value) { 196 enforcementOverride = value; 197 } 198 199 /** Test seam: restore the pre-activation state. */ 200 static void resetForTesting() { 201 current = new GspActivation(GspActivationStatus.MISSING, null, null, 0L); 202 } 203 204 private static String newAuditId() { 205 byte[] raw = new byte[8]; 206 AUDIT_IDS.nextBytes(raw); 207 StringBuilder sb = new StringBuilder(16); 208 for (byte b : raw) { 209 sb.append(Character.forDigit((b >> 4) & 0xF, 16)); 210 sb.append(Character.forDigit(b & 0xF, 16)); 211 } 212 return sb.toString(); 213 } 214}