feat(pki-server): authorize signed OCSP requests
Add per-responder signed-request policies, strict request-signature and requester-certificate validation, cryptographic principal mapping and scoped OCSP query authorization. Preserve public unsigned responder behavior while isolating requester, transport and administrative identities.
This commit is contained in:
@@ -12,7 +12,9 @@ An OCSP responder is a durable server-control record. Its alias binds exactly
|
|||||||
one realm, logical authority, issuer generation, responder certificate,
|
one realm, logical authority, issuer generation, responder certificate,
|
||||||
`KeyRef`, explicit chain path, signature algorithm/binding commitment, nonce
|
`KeyRef`, explicit chain path, signature algorithm/binding commitment, nonce
|
||||||
policy, response-validity policy, accepted CertID hashes, and finite request
|
policy, response-validity policy, accepted CertID hashes, and finite request
|
||||||
bounds. Activation revalidates those dependencies and freezes their
|
bounds. Each record also carries an explicit request-authentication mode and,
|
||||||
|
for signed modes, a finite immutable requester trust, mapping, signature,
|
||||||
|
revocation, and chain-bound policy. Activation revalidates those dependencies and freezes their
|
||||||
commitments. There is no first/last issuer selection, filename inference,
|
commitments. There is no first/last issuer selection, filename inference,
|
||||||
timestamp inference, runtime path building, or fallback signing algorithm.
|
timestamp inference, runtime path building, or fallback signing algorithm.
|
||||||
|
|
||||||
@@ -66,6 +68,77 @@ nonce echo:
|
|||||||
nonce-bearing responses use no-store
|
nonce-bearing responses use no-store
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Request authentication
|
||||||
|
|
||||||
|
Every responder selects exactly one mode:
|
||||||
|
|
||||||
|
- `UNSIGNED_ONLY` is the default public responder policy. Unsigned requests are
|
||||||
|
accepted and syntactically signed requests receive OCSP `unauthorized`.
|
||||||
|
- `UNSIGNED_OR_AUTHORIZED_SIGNED` preserves anonymous requests, but any signed
|
||||||
|
request must authenticate and authorize completely; it never falls back to
|
||||||
|
anonymous handling.
|
||||||
|
- `AUTHORIZED_SIGNED_REQUIRED` returns OCSP `sigRequired` for unsigned requests
|
||||||
|
and accepts only authorized signed requests.
|
||||||
|
|
||||||
|
Signed request authentication is protocol-scoped. ZeroEcho retains the exact
|
||||||
|
canonical `tbsRequest` DER, requires a canonical `directoryName`
|
||||||
|
`requestorName` equal to the signer certificate subject, validates the bounded
|
||||||
|
included signer chain against configured trust selectors, and verifies the
|
||||||
|
signature using an active `OCSP_REQUEST_SIGNATURE` binding. Current standard
|
||||||
|
bindings permit ECDSA/SHA-256, Ed25519, RSA PKCS#1/SHA-256 and strict
|
||||||
|
RSA-PSS/SHA-256 when explicitly listed. SHA-1 request signatures are rejected.
|
||||||
|
|
||||||
|
Trust and authorization are independent. A cryptographic certificate, SPKI, or
|
||||||
|
managed issuer-generation-plus-serial commitment must map to exactly one enabled
|
||||||
|
persisted `SecurityPrincipal`. That principal needs an explicit
|
||||||
|
`OCSP_STATUS_QUERY` grant scoped to the exact realm, responder, authority, and
|
||||||
|
issuer generation. Explicit deny wins. `OCSP_ADMINISTER` grants no query right,
|
||||||
|
and query authority grants no administration, content, search, ownership,
|
||||||
|
disclosure, or PII right. Multi-entry authorization is all-or-nothing.
|
||||||
|
|
||||||
|
Requester certificate revocation uses one explicit policy:
|
||||||
|
|
||||||
|
- `LOCAL_AUTHORITATIVE_REQUIRED` requires a managed ZeroEcho credential and
|
||||||
|
current local revocation state. It is the recommended closed-enterprise mode.
|
||||||
|
- `LOCAL_AUTHORITATIVE_IF_MANAGED` checks managed credentials locally and uses
|
||||||
|
validity plus configured trust for external credentials.
|
||||||
|
- `VALIDITY_AND_TRUST_ONLY` performs no revocation check and must be selected
|
||||||
|
deliberately with its stale-credential risk understood.
|
||||||
|
|
||||||
|
ZeroEcho does not follow AIA, CRL, or OCSP URLs while authenticating a requester
|
||||||
|
and never calls the responder recursively. A request signature authenticates
|
||||||
|
integrity and requester identity; it does not replace nonce freshness. Replay of
|
||||||
|
a permitted read-only request remains valid when nonce policy permits it.
|
||||||
|
`REQUIRED` nonce responders keep a finite derived process-local replay window;
|
||||||
|
restart may discard that operational window without changing PKI authority.
|
||||||
|
|
||||||
|
Safe policy examples omit real identities and commitments:
|
||||||
|
|
||||||
|
```text
|
||||||
|
public unsigned:
|
||||||
|
requestAuthenticationMode = UNSIGNED_ONLY
|
||||||
|
|
||||||
|
mixed transition:
|
||||||
|
requestAuthenticationMode = UNSIGNED_OR_AUTHORIZED_SIGNED
|
||||||
|
requesterRevocationPolicy = LOCAL_AUTHORITATIVE_REQUIRED
|
||||||
|
|
||||||
|
closed managed enterprise:
|
||||||
|
requestAuthenticationMode = AUTHORIZED_SIGNED_REQUIRED
|
||||||
|
trustSelectors = [managed issuer generations]
|
||||||
|
requiredPermission = OCSP_STATUS_QUERY
|
||||||
|
requesterRevocationPolicy = LOCAL_AUTHORITATIVE_REQUIRED
|
||||||
|
|
||||||
|
closed external trust:
|
||||||
|
requestAuthenticationMode = AUTHORIZED_SIGNED_REQUIRED
|
||||||
|
trustSelectors = [configured trust anchors]
|
||||||
|
requesterRevocationPolicy = LOCAL_AUTHORITATIVE_IF_MANAGED
|
||||||
|
```
|
||||||
|
|
||||||
|
Migration is explicit: replace the pre-release responder record with its desired
|
||||||
|
mode and complete requester policy, validate scoped grants, and activate it
|
||||||
|
through the configured approval workflow. The obsolete schema is rejected; no
|
||||||
|
mode is inferred and no compatibility reader exists.
|
||||||
|
|
||||||
## Requests and responses
|
## Requests and responses
|
||||||
|
|
||||||
POST uses `Content-Type: application/ocsp-request`. GET uses an unpadded,
|
POST uses `Content-Type: application/ocsp-request`. GET uses an unpadded,
|
||||||
@@ -76,10 +149,10 @@ canonical URL-safe Base64 request path:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Query-string requests and alternate Base64 normalization are rejected. The
|
Query-string requests and alternate Base64 normalization are rejected. The
|
||||||
strict DER parser accepts unsigned requests only, consumes the complete input,
|
strict DER parser consumes the complete input, rejects all per-entry extensions
|
||||||
rejects duplicate or unknown critical extensions, and enforces configured byte
|
and duplicate or unknown critical request extensions, and enforces configured
|
||||||
and entry bounds. Signed OCSP requests are rejected in this release; a request
|
byte, entry, signer-certificate, and chain bounds. Signed requests additionally
|
||||||
signature is never treated as administrative or certificate-owner authority.
|
require the exact canonical optional-signature structure described above.
|
||||||
|
|
||||||
Configured CertID hashing supports SHA-1 and SHA-256. SHA-1 is permitted only
|
Configured CertID hashing supports SHA-1 and SHA-256. SHA-1 is permitted only
|
||||||
for RFC-compatible issuer-name/key hashes. It is never enabled as a certificate
|
for RFC-compatible issuer-name/key hashes. It is never enabled as a certificate
|
||||||
@@ -98,18 +171,22 @@ recovery-required authority returns a transient protocol/transport failure, not
|
|||||||
|
|
||||||
Successful responses are strict signed DER with
|
Successful responses are strict signed DER with
|
||||||
`application/ocsp-response`, `nosniff`, `producedAt`, `thisUpdate`, and
|
`application/ocsp-response`, `nosniff`, `producedAt`, `thisUpdate`, and
|
||||||
`nextUpdate`. Nonce-free responses carry a strong ETag and bounded public cache
|
`nextUpdate`. Nonce-free unsigned responses carry a strong ETag and bounded public cache
|
||||||
policy. An echoed nonce is returned exactly and makes the response non-cacheable.
|
policy. An echoed nonce is returned exactly and makes the response non-cacheable.
|
||||||
|
Every authenticated signed response uses `Cache-Control: private, no-store` and
|
||||||
|
`Pragma: no-cache`, even without a nonce; shared caches never store it.
|
||||||
No request DER, response DER, serial, issuer hash, nonce, certificate identity,
|
No request DER, response DER, serial, issuer hash, nonce, certificate identity,
|
||||||
key reference, or parser/provider exception is logged or audited.
|
key reference, or parser/provider exception is logged or audited.
|
||||||
|
|
||||||
## Deployment topology and capacity
|
## Deployment topology and capacity
|
||||||
|
|
||||||
Direct mode permits anonymous OCSP over the public TLS listener. An optional
|
Direct mode permits anonymous OCSP over the public TLS listener. An optional
|
||||||
public client certificate does not change certificate status, and forwarded
|
public client certificate neither replaces nor supplements the signed requester
|
||||||
identity headers are rejected. In trusted-reverse-proxy mode the backend proxy
|
and does not change certificate status; forwarded identity headers remain
|
||||||
hop still requires mTLS and the dedicated forwarding permission; proxy and
|
rejected. In trusted-reverse-proxy mode the backend proxy hop still requires
|
||||||
forwarded end-client identities do not authorize or select OCSP status. Source
|
mTLS and the dedicated forwarding permission. The proxy transport principal,
|
||||||
|
forwarded administrative principal, and signed OCSP requester remain distinct
|
||||||
|
and do not inherit one another's permissions or select OCSP status. Source
|
||||||
IP, `Forwarded`, and `X-Forwarded-For` are never responder authority.
|
IP, `Forwarded`, and `X-Forwarded-For` are never responder authority.
|
||||||
|
|
||||||
OCSP has a separate bounded runtime lane even though it shares the public
|
OCSP has a separate bounded runtime lane even though it shares the public
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ public final class AuthorizationEngine {
|
|||||||
&& !grant.scope().issuerId().equals(request.resource().scope().issuerId())) return false;
|
&& !grant.scope().issuerId().equals(request.resource().scope().issuerId())) return false;
|
||||||
if (grant.scope().profileId().isPresent()
|
if (grant.scope().profileId().isPresent()
|
||||||
&& !grant.scope().profileId().equals(request.resource().scope().profileId())) return false;
|
&& !grant.scope().profileId().equals(request.resource().scope().profileId())) return false;
|
||||||
|
if (grant.scope().responderId().isPresent()
|
||||||
|
&& !grant.scope().responderId().equals(request.resource().scope().responderId())) return false;
|
||||||
return grant.relationship() == Permission.Relationship.ANY
|
return grant.relationship() == Permission.Relationship.ANY
|
||||||
|| request.relationship() == Permission.Relationship.OWN
|
|| request.relationship() == Permission.Relationship.OWN
|
||||||
&& request.resource().ownedBy(request.principal());
|
&& request.resource().ownedBy(request.principal());
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2026, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
* are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software without
|
||||||
|
* specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.pki.server;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1Primitive;
|
||||||
|
import org.bouncycastle.asn1.DERNull;
|
||||||
|
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
|
||||||
|
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
||||||
|
import org.bouncycastle.asn1.pkcs.RSASSAPSSparams;
|
||||||
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.operator.ContentVerifier;
|
||||||
|
import org.bouncycastle.operator.ContentVerifierProvider;
|
||||||
|
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
|
||||||
|
import org.bouncycastle.operator.bc.BcECContentVerifierProviderBuilder;
|
||||||
|
import org.bouncycastle.operator.bc.BcEdDSAContentVerifierProviderBuilder;
|
||||||
|
import org.bouncycastle.operator.bc.BcRSAContentVerifierProviderBuilder;
|
||||||
|
|
||||||
|
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||||
|
import zeroecho.pki.api.PkiId;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.credential.Credential;
|
||||||
|
import zeroecho.pki.api.revocation.RevocationState;
|
||||||
|
import zeroecho.pki.application.OcspResponseService;
|
||||||
|
import zeroecho.pki.application.PkiRepositoryContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transport-neutral signed-OCSP requester authentication and scoped query
|
||||||
|
* authorization. Certificate trust proves eligibility only; an enabled persisted
|
||||||
|
* principal and an explicit {@link Permission.Action#OCSP_STATUS_QUERY} grant are
|
||||||
|
* independently required.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.CyclomaticComplexity",
|
||||||
|
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.FieldDeclarationsShouldBeAtStartOfClass",
|
||||||
|
"PMD.ControlStatementBraces", "PMD.PreserveStackTrace", "PMD.AvoidLiteralsInIfCondition",
|
||||||
|
"PMD.DataClass" })
|
||||||
|
public final class OcspRequesterService {
|
||||||
|
private static final int MAXIMUM_CERTIFICATE_BYTES = 1_048_576;
|
||||||
|
|
||||||
|
/** Exact bounded cryptographic fields retained from one canonical request. */
|
||||||
|
public record SignedRequest(byte[] tbsRequest, byte[] requestorName, byte[] signatureAlgorithm,
|
||||||
|
byte[] signature, List<byte[]> certificates) {
|
||||||
|
/** Snapshots all security-sensitive input bytes. */
|
||||||
|
public SignedRequest {
|
||||||
|
tbsRequest = requireBytes(tbsRequest, "tbsRequest", 1_048_576);
|
||||||
|
requestorName = requireBytes(requestorName, "requestorName", 65_536);
|
||||||
|
signatureAlgorithm = requireBytes(signatureAlgorithm, "signatureAlgorithm", 4_096);
|
||||||
|
signature = requireBytes(signature, "signature", 65_536);
|
||||||
|
List<byte[]> exact = new ArrayList<>();
|
||||||
|
for (byte[] certificate : Objects.requireNonNull(certificates, "certificates")) {
|
||||||
|
exact.add(requireBytes(certificate, "certificate", MAXIMUM_CERTIFICATE_BYTES));
|
||||||
|
}
|
||||||
|
certificates = List.copyOf(exact);
|
||||||
|
}
|
||||||
|
@Override public byte[] tbsRequest() { return tbsRequest.clone(); }
|
||||||
|
@Override public byte[] requestorName() { return requestorName.clone(); }
|
||||||
|
@Override public byte[] signatureAlgorithm() { return signatureAlgorithm.clone(); }
|
||||||
|
@Override public byte[] signature() { return signature.clone(); }
|
||||||
|
@Override public List<byte[]> certificates() {
|
||||||
|
return certificates.stream().map(byte[]::clone).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Safe successful authentication result. */
|
||||||
|
public record Authorized(String principalId) {
|
||||||
|
/** Validates the mapped principal identity. */
|
||||||
|
public Authorized { Permission.requirePrincipal(principalId); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Safe authentication or authorization rejection. */
|
||||||
|
public static final class UnauthorizedException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final String classification;
|
||||||
|
/** Creates a redacted rejection. */
|
||||||
|
public UnauthorizedException() { this("UNSPECIFIED"); }
|
||||||
|
private UnauthorizedException(String classification) {
|
||||||
|
super("OCSP requester is unauthorized");
|
||||||
|
this.classification = classification;
|
||||||
|
}
|
||||||
|
/** @return safe non-secret internal rejection classification */
|
||||||
|
public String classification() { return classification; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dependency corruption or recovery-required authorization authority. */
|
||||||
|
public static final class UnavailableException extends RuntimeException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
/** Creates a redacted unavailable result. */
|
||||||
|
public UnavailableException() { super("OCSP requester authority is unavailable"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServerRealmContext realm;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
/** Creates one realm-bound requester authentication service. */
|
||||||
|
public OcspRequesterService(ServerRealmContext realm, Clock clock) {
|
||||||
|
this.realm = Objects.requireNonNull(realm, "realm");
|
||||||
|
this.clock = Objects.requireNonNull(clock, "clock");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates and authorizes every CertID in one signed request. Any failure
|
||||||
|
* rejects the whole request without revealing the failed entry.
|
||||||
|
*/
|
||||||
|
public Authorized authenticate(OcspResponderService.Responder responder, SignedRequest request,
|
||||||
|
List<OcspResponseService.CertId> requests) {
|
||||||
|
try {
|
||||||
|
OcspResponderService.RequesterTrustPolicy policy = responder.requesterTrustPolicy().orElseThrow();
|
||||||
|
List<X509Certificate> chain = certificates(request.certificates());
|
||||||
|
X509Certificate signer = chain.getFirst();
|
||||||
|
validateSigner(signer, request.requestorName());
|
||||||
|
validateTrust(policy, chain);
|
||||||
|
verifySignature(policy, signer, request);
|
||||||
|
String principalId = mapPrincipal(policy, signer);
|
||||||
|
SecurityPrincipal principal = realm.principal(principalId);
|
||||||
|
if (!principal.enabled()) throw new UnauthorizedException("PRINCIPAL_DISABLED");
|
||||||
|
validateRevocation(policy, signer);
|
||||||
|
for (OcspResponseService.CertId certId : requests) {
|
||||||
|
requireIssuer(responder, certId);
|
||||||
|
if (!realm.authorizeOcspQuery(principalId, responder).allowed()) {
|
||||||
|
throw new UnauthorizedException("QUERY_DENIED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Authorized(principalId);
|
||||||
|
} catch (UnauthorizedException rejection) {
|
||||||
|
throw rejection;
|
||||||
|
} catch (IllegalStateException unavailable) {
|
||||||
|
throw new UnavailableException();
|
||||||
|
} catch (RuntimeException | CertificateException | IOException failure) {
|
||||||
|
throw new UnauthorizedException("VALIDATION_REJECTED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSigner(X509Certificate signer, byte[] requestorName) throws CertificateException {
|
||||||
|
signer.checkValidity(Date.from(clock.instant()));
|
||||||
|
requireCanonical(signer);
|
||||||
|
if (signer.hasUnsupportedCriticalExtension()) throw new CertificateException("Unsupported critical extension");
|
||||||
|
boolean[] usage = signer.getKeyUsage();
|
||||||
|
if (usage != null && (usage.length == 0 || !usage[0])) throw new CertificateException("Signing usage denied");
|
||||||
|
if (!Arrays.equals(requestorName, signer.getSubjectX500Principal().getEncoded())) {
|
||||||
|
throw new UnauthorizedException("REQUESTOR_NAME");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateTrust(OcspResponderService.RequesterTrustPolicy policy, List<X509Certificate> chain)
|
||||||
|
throws CertificateException, IOException {
|
||||||
|
int matches = 0;
|
||||||
|
String signerCommitment = digest(chain.getFirst().getEncoded());
|
||||||
|
for (OcspResponderService.TrustSelector selector : policy.trustSelectors()) {
|
||||||
|
if (selector.type() == OcspResponderService.TrustSelectorType.EXACT_REQUESTER_CERTIFICATE_COMMITMENTS) {
|
||||||
|
if (selector.identities().contains(signerCommitment) && chain.size() == 1) matches++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String identity : selector.identities()) {
|
||||||
|
X509Certificate anchor = selector.type() == OcspResponderService.TrustSelectorType.MANAGED_ISSUER_GENERATIONS
|
||||||
|
? issuerCertificate(new PkiId(identity)) : credentialCertificate(new PkiId(identity));
|
||||||
|
if (validPath(chain, anchor)) matches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches != 1) throw new UnauthorizedException("TRUST_PATH");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validPath(List<X509Certificate> chain, X509Certificate anchor) {
|
||||||
|
try {
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
|
for (X509Certificate certificate : chain) {
|
||||||
|
certificate.checkValidity(Date.from(clock.instant()));
|
||||||
|
requireCanonical(certificate);
|
||||||
|
if (certificate.hasUnsupportedCriticalExtension()) return false;
|
||||||
|
if (!seen.add(digest(certificate.getEncoded()))) return false;
|
||||||
|
}
|
||||||
|
anchor.checkValidity(Date.from(clock.instant()));
|
||||||
|
requireCanonical(anchor);
|
||||||
|
if (anchor.hasUnsupportedCriticalExtension()) return false;
|
||||||
|
for (int index = 0; index + 1 < chain.size(); index++) {
|
||||||
|
if (chain.get(index + 1).getBasicConstraints() < 0) return false;
|
||||||
|
boolean[] usage = chain.get(index + 1).getKeyUsage();
|
||||||
|
if (usage != null && (usage.length <= 5 || !usage[5])) return false;
|
||||||
|
verifyIssuedBy(chain.get(index), chain.get(index + 1));
|
||||||
|
}
|
||||||
|
X509Certificate last = chain.getLast();
|
||||||
|
if (Arrays.equals(last.getEncoded(), anchor.getEncoded())) return true;
|
||||||
|
if (anchor.getBasicConstraints() < 0) return false;
|
||||||
|
boolean[] anchorUsage = anchor.getKeyUsage();
|
||||||
|
if (anchorUsage != null && (anchorUsage.length <= 5 || !anchorUsage[5])) return false;
|
||||||
|
verifyIssuedBy(last, anchor);
|
||||||
|
return true;
|
||||||
|
} catch (CertificateException | RuntimeException failure) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyIssuedBy(X509Certificate certificate, X509Certificate issuer)
|
||||||
|
throws CertificateException {
|
||||||
|
if (!Arrays.equals(certificate.getIssuerX500Principal().getEncoded(),
|
||||||
|
issuer.getSubjectX500Principal().getEncoded())) throw new CertificateException("Issuer mismatch");
|
||||||
|
try { certificate.verify(issuer.getPublicKey()); }
|
||||||
|
catch (java.security.GeneralSecurityException failure) { throw new CertificateException("Signature mismatch"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifySignature(OcspResponderService.RequesterTrustPolicy policy, X509Certificate signer,
|
||||||
|
SignedRequest request) throws IOException {
|
||||||
|
AlgorithmIdentifier algorithm = AlgorithmIdentifier.getInstance(ASN1Primitive.fromByteArray(
|
||||||
|
request.signatureAlgorithm()));
|
||||||
|
X509AlgorithmBinding selected = null;
|
||||||
|
for (OcspResponderService.RequestSignatureBinding reference : policy.signatureBindings()) {
|
||||||
|
X509AlgorithmBinding binding = realm.session().algorithmBindings().require(reference.bindingId(),
|
||||||
|
reference.commitment());
|
||||||
|
if (binding.role() == X509AlgorithmBinding.Role.OCSP_REQUEST_SIGNATURE
|
||||||
|
&& binding.oid().equals(algorithm.getAlgorithm().getId()) && parametersMatch(binding, algorithm)) {
|
||||||
|
if (selected != null) throw new UnauthorizedException("BINDING_AMBIGUOUS");
|
||||||
|
selected = binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected == null || !supported(selected)) throw new UnauthorizedException("BINDING_REJECTED");
|
||||||
|
try {
|
||||||
|
ContentVerifier verifier = verifier(selected, signer).get(algorithm);
|
||||||
|
verifier.getOutputStream().write(request.tbsRequest());
|
||||||
|
verifier.getOutputStream().close();
|
||||||
|
if (!verifier.verify(request.signature())) throw new UnauthorizedException("SIGNATURE_INVALID");
|
||||||
|
} catch (org.bouncycastle.operator.OperatorCreationException | CertificateException failure) {
|
||||||
|
throw new UnauthorizedException("SIGNATURE_PROVIDER");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContentVerifierProvider verifier(X509AlgorithmBinding binding, X509Certificate signer)
|
||||||
|
throws CertificateException, IOException, org.bouncycastle.operator.OperatorCreationException {
|
||||||
|
X509CertificateHolder holder = new X509CertificateHolder(signer.getEncoded());
|
||||||
|
if (binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.ECDSA_SHA256)) {
|
||||||
|
return new BcECContentVerifierProviderBuilder(DefaultDigestAlgorithmIdentifierFinder.INSTANCE)
|
||||||
|
.build(holder);
|
||||||
|
}
|
||||||
|
if (binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.ED25519_SIGNATURE)) {
|
||||||
|
return new BcEdDSAContentVerifierProviderBuilder().build(holder);
|
||||||
|
}
|
||||||
|
return new BcRSAContentVerifierProviderBuilder(DefaultDigestAlgorithmIdentifierFinder.INSTANCE)
|
||||||
|
.build(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean supported(X509AlgorithmBinding binding) {
|
||||||
|
return binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.ECDSA_SHA256)
|
||||||
|
|| binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.ED25519_SIGNATURE)
|
||||||
|
|| binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256)
|
||||||
|
|| binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.RSA_PSS_SHA256);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean parametersMatch(X509AlgorithmBinding binding, AlgorithmIdentifier algorithm) {
|
||||||
|
return switch (binding.parameterRule()) {
|
||||||
|
case ABSENT -> algorithm.getParameters() == null;
|
||||||
|
case DER_NULL -> DERNull.INSTANCE.equals(algorithm.getParameters());
|
||||||
|
case STRUCTURED_DER -> strictPss(binding, algorithm);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean strictPss(X509AlgorithmBinding binding, AlgorithmIdentifier algorithm) {
|
||||||
|
if (!binding.algorithmIdentity().equals(BootstrapAlgorithmIdentities.RSA_PSS_SHA256)
|
||||||
|
|| !PKCSObjectIdentifiers.id_RSASSA_PSS.equals(algorithm.getAlgorithm())
|
||||||
|
|| algorithm.getParameters() == null) return false;
|
||||||
|
try {
|
||||||
|
RSASSAPSSparams parameters = RSASSAPSSparams.getInstance(algorithm.getParameters());
|
||||||
|
AlgorithmIdentifier hash = parameters.getHashAlgorithm();
|
||||||
|
AlgorithmIdentifier mask = parameters.getMaskGenAlgorithm();
|
||||||
|
AlgorithmIdentifier maskHash = AlgorithmIdentifier.getInstance(mask.getParameters());
|
||||||
|
return NISTObjectIdentifiers.id_sha256.equals(hash.getAlgorithm())
|
||||||
|
&& DERNull.INSTANCE.equals(hash.getParameters())
|
||||||
|
&& PKCSObjectIdentifiers.id_mgf1.equals(mask.getAlgorithm())
|
||||||
|
&& NISTObjectIdentifiers.id_sha256.equals(maskHash.getAlgorithm())
|
||||||
|
&& DERNull.INSTANCE.equals(maskHash.getParameters())
|
||||||
|
&& BigInteger.valueOf(32).equals(parameters.getSaltLength())
|
||||||
|
&& BigInteger.ONE.equals(parameters.getTrailerField());
|
||||||
|
} catch (RuntimeException failure) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mapPrincipal(OcspResponderService.RequesterTrustPolicy policy, X509Certificate signer)
|
||||||
|
throws CertificateException {
|
||||||
|
String certificate = digest(signer.getEncoded());
|
||||||
|
String spki = digest(signer.getPublicKey().getEncoded());
|
||||||
|
MessageDigest issuerSerial = sha256();
|
||||||
|
issuerSerial.update(signer.getIssuerX500Principal().getEncoded());
|
||||||
|
issuerSerial.update((byte) 0);
|
||||||
|
issuerSerial.update(positiveSerial(signer).toByteArray());
|
||||||
|
String exactIssuerSerial = HexFormat.of().formatHex(issuerSerial.digest());
|
||||||
|
List<OcspResponderService.PrincipalMapping> matches = policy.principalMappings().stream()
|
||||||
|
.filter(mapping -> switch (mapping.type()) {
|
||||||
|
case SHA256_CERTIFICATE_COMMITMENT -> mapping.commitment().equals(certificate);
|
||||||
|
case SHA256_SPKI_COMMITMENT -> mapping.commitment().equals(spki);
|
||||||
|
case ISSUER_AND_SERIAL_COMMITMENT -> mapping.commitment().equals(exactIssuerSerial);
|
||||||
|
}).toList();
|
||||||
|
if (matches.size() != 1) throw new UnauthorizedException("PRINCIPAL_MAPPING");
|
||||||
|
return matches.getFirst().principalId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRevocation(OcspResponderService.RequesterTrustPolicy policy, X509Certificate signer)
|
||||||
|
throws CertificateException {
|
||||||
|
Optional<Credential> managed = managedCredential(policy, signer);
|
||||||
|
if (policy.revocationPolicy() == OcspResponderService.RequesterRevocationPolicy.LOCAL_AUTHORITATIVE_REQUIRED
|
||||||
|
&& managed.isEmpty()) throw new UnauthorizedException("LOCAL_SIGNER_REQUIRED");
|
||||||
|
if (policy.revocationPolicy() != OcspResponderService.RequesterRevocationPolicy.VALIDITY_AND_TRUST_ONLY
|
||||||
|
&& managed.isPresent()) {
|
||||||
|
RevocationState state = realm.session().revocations().get(managed.orElseThrow().credentialId())
|
||||||
|
.map(record -> record.transition().state()).orElse(RevocationState.CLEAR);
|
||||||
|
if (state != RevocationState.CLEAR) throw new UnauthorizedException("SIGNER_REVOKED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Credential> managedCredential(OcspResponderService.RequesterTrustPolicy policy,
|
||||||
|
X509Certificate signer) throws CertificateException {
|
||||||
|
BigInteger serial = positiveSerial(signer);
|
||||||
|
String commitment = digest(signer.getEncoded());
|
||||||
|
List<Credential> matches = new ArrayList<>();
|
||||||
|
for (OcspResponderService.TrustSelector selector : policy.trustSelectors()) {
|
||||||
|
if (selector.type() != OcspResponderService.TrustSelectorType.MANAGED_ISSUER_GENERATIONS) continue;
|
||||||
|
for (String identity : selector.identities()) {
|
||||||
|
realm.session().repository().credential(new PkiId(identity), serial)
|
||||||
|
.filter(credential -> credentialCommitment(credential).equals(commitment)).ifPresent(matches::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.size() > 1) throw new UnauthorizedException("MANAGED_SIGNER_AMBIGUOUS");
|
||||||
|
return matches.stream().findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireIssuer(OcspResponderService.Responder responder, OcspResponseService.CertId certId) {
|
||||||
|
X509Certificate issuer = issuerCertificate(responder.issuerId());
|
||||||
|
try {
|
||||||
|
byte[] name = MessageDigest.getInstance(certId.hash() == OcspResponseService.CertIdHash.SHA1
|
||||||
|
? "SHA-1" : "SHA-256").digest(issuer.getSubjectX500Principal().getEncoded());
|
||||||
|
X509CertificateHolder holder = new X509CertificateHolder(issuer.getEncoded());
|
||||||
|
byte[] key = MessageDigest.getInstance(certId.hash() == OcspResponseService.CertIdHash.SHA1
|
||||||
|
? "SHA-1" : "SHA-256").digest(holder.getSubjectPublicKeyInfo().getPublicKeyData().getBytes());
|
||||||
|
if (!MessageDigest.isEqual(name, certId.issuerNameHash())
|
||||||
|
|| !MessageDigest.isEqual(key, certId.issuerKeyHash())) {
|
||||||
|
throw new UnauthorizedException("ISSUER_SCOPE");
|
||||||
|
}
|
||||||
|
} catch (CertificateException | IOException | NoSuchAlgorithmException failure) {
|
||||||
|
throw new UnavailableException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate issuerCertificate(PkiId issuerId) {
|
||||||
|
PkiId credentialId = realm.session().repository().issuer(issuerId).orElseThrow().credentialId();
|
||||||
|
return credentialCertificate(credentialId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate credentialCertificate(PkiId credentialId) {
|
||||||
|
try (PkiRepositoryContent content = realm.session().repository().openCredential(credentialId);
|
||||||
|
InputStream input = content.openStream()) {
|
||||||
|
byte[] encoded = input.readNBytes(MAXIMUM_CERTIFICATE_BYTES + 1);
|
||||||
|
if (encoded.length > MAXIMUM_CERTIFICATE_BYTES || input.read() != -1) throw new IllegalStateException();
|
||||||
|
return parseCertificate(encoded);
|
||||||
|
} catch (IOException | CertificateException failure) { throw new IllegalStateException(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private String credentialCommitment(Credential credential) {
|
||||||
|
try {
|
||||||
|
return digest(credentialCertificate(credential.credentialId()).getEncoded());
|
||||||
|
} catch (CertificateException failure) {
|
||||||
|
throw new IllegalStateException("Managed requester certificate is unavailable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<X509Certificate> certificates(List<byte[]> encoded) throws CertificateException {
|
||||||
|
if (encoded.isEmpty()) throw new CertificateException("Signer certificate is required");
|
||||||
|
List<X509Certificate> result = new ArrayList<>(encoded.size());
|
||||||
|
for (byte[] certificate : encoded) result.add(parseCertificate(certificate));
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate parseCertificate(byte[] encoded) throws CertificateException {
|
||||||
|
ByteArrayInputStream input = new ByteArrayInputStream(encoded);
|
||||||
|
X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(input);
|
||||||
|
if (input.available() != 0 || !Arrays.equals(encoded, certificate.getEncoded())) {
|
||||||
|
throw new CertificateException("Certificate is not canonical");
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireCanonical(X509Certificate certificate) throws CertificateException {
|
||||||
|
parseCertificate(certificate.getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigInteger positiveSerial(X509Certificate certificate) {
|
||||||
|
if (certificate.getSerialNumber().signum() <= 0) throw new UnauthorizedException("SERIAL_INVALID");
|
||||||
|
return certificate.getSerialNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String digest(byte[] value) { return HexFormat.of().formatHex(sha256().digest(value)); }
|
||||||
|
private static MessageDigest sha256() {
|
||||||
|
try { return MessageDigest.getInstance("SHA-256"); }
|
||||||
|
catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException("SHA-256 unavailable"); }
|
||||||
|
}
|
||||||
|
private static byte[] requireBytes(byte[] value, String field, int maximum) {
|
||||||
|
Objects.requireNonNull(value, field);
|
||||||
|
if (value.length == 0 || value.length > maximum) throw new IllegalArgumentException(field + " is invalid");
|
||||||
|
return value.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ import java.util.HexFormat;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.x509.Extension;
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
@@ -60,6 +61,7 @@ import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
|
|||||||
import zeroecho.pki.api.KeyRef;
|
import zeroecho.pki.api.KeyRef;
|
||||||
import zeroecho.pki.api.PkiId;
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
import zeroecho.pki.api.ca.IssuerChainPath;
|
import zeroecho.pki.api.ca.IssuerChainPath;
|
||||||
import zeroecho.pki.api.ca.IssuerGeneration;
|
import zeroecho.pki.api.ca.IssuerGeneration;
|
||||||
import zeroecho.pki.application.OcspResponseService;
|
import zeroecho.pki.application.OcspResponseService;
|
||||||
@@ -72,6 +74,123 @@ import zeroecho.pki.application.PkiRepository;
|
|||||||
"PMD.PreserveStackTrace", "PMD.AvoidCatchingGenericException", "PMD.UseEnumCollections",
|
"PMD.PreserveStackTrace", "PMD.AvoidCatchingGenericException", "PMD.UseEnumCollections",
|
||||||
"PMD.ExceptionAsFlowControl" })
|
"PMD.ExceptionAsFlowControl" })
|
||||||
public final class OcspResponderService {
|
public final class OcspResponderService {
|
||||||
|
/** Closed signed-request authentication policy. */
|
||||||
|
public enum RequestAuthenticationMode {
|
||||||
|
UNSIGNED_ONLY(1), UNSIGNED_OR_AUTHORIZED_SIGNED(2), AUTHORIZED_SIGNED_REQUIRED(3);
|
||||||
|
private final int code;
|
||||||
|
RequestAuthenticationMode(int code) { this.code = code; }
|
||||||
|
/** Stable persistence code. */ public int code() { return code; }
|
||||||
|
/** Resolves an exact stable code. */
|
||||||
|
public static RequestAuthenticationMode fromCode(int code) {
|
||||||
|
return switch (code) { case 1 -> UNSIGNED_ONLY; case 2 -> UNSIGNED_OR_AUTHORIZED_SIGNED;
|
||||||
|
case 3 -> AUTHORIZED_SIGNED_REQUIRED;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown OCSP request-authentication mode code"); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closed local requester-certificate revocation behavior. */
|
||||||
|
public enum RequesterRevocationPolicy {
|
||||||
|
LOCAL_AUTHORITATIVE_REQUIRED(1), LOCAL_AUTHORITATIVE_IF_MANAGED(2), VALIDITY_AND_TRUST_ONLY(3);
|
||||||
|
private final int code;
|
||||||
|
RequesterRevocationPolicy(int code) { this.code = code; }
|
||||||
|
/** Stable persistence code. */ public int code() { return code; }
|
||||||
|
/** Resolves an exact stable code. */
|
||||||
|
public static RequesterRevocationPolicy fromCode(int code) {
|
||||||
|
return switch (code) { case 1 -> LOCAL_AUTHORITATIVE_REQUIRED;
|
||||||
|
case 2 -> LOCAL_AUTHORITATIVE_IF_MANAGED; case 3 -> VALIDITY_AND_TRUST_ONLY;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown OCSP requester revocation policy code"); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Repository-backed requester trust selector. */
|
||||||
|
@SuppressWarnings("PMD.LongVariable")
|
||||||
|
public enum TrustSelectorType {
|
||||||
|
MANAGED_ISSUER_GENERATIONS(1), CONFIGURED_TRUST_ANCHORS(2), EXACT_REQUESTER_CERTIFICATE_COMMITMENTS(3);
|
||||||
|
private final int code;
|
||||||
|
TrustSelectorType(int code) { this.code = code; }
|
||||||
|
/** Stable persistence code. */ public int code() { return code; }
|
||||||
|
/** Resolves an exact stable code. */
|
||||||
|
public static TrustSelectorType fromCode(int code) {
|
||||||
|
return switch (code) { case 1 -> MANAGED_ISSUER_GENERATIONS; case 2 -> CONFIGURED_TRUST_ANCHORS;
|
||||||
|
case 3 -> EXACT_REQUESTER_CERTIFICATE_COMMITMENTS;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown OCSP requester trust-selector code"); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cryptographic requester-to-principal mapping type. */
|
||||||
|
public enum PrincipalMappingType {
|
||||||
|
SHA256_CERTIFICATE_COMMITMENT(1), SHA256_SPKI_COMMITMENT(2), ISSUER_AND_SERIAL_COMMITMENT(3);
|
||||||
|
private final int code;
|
||||||
|
PrincipalMappingType(int code) { this.code = code; }
|
||||||
|
/** Stable persistence code. */ public int code() { return code; }
|
||||||
|
/** Resolves an exact stable code. */
|
||||||
|
public static PrincipalMappingType fromCode(int code) {
|
||||||
|
return switch (code) { case 1 -> SHA256_CERTIFICATE_COMMITMENT;
|
||||||
|
case 2 -> SHA256_SPKI_COMMITMENT; case 3 -> ISSUER_AND_SERIAL_COMMITMENT;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown OCSP requester mapping code"); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One explicit repository-backed trust selector. */
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
public record TrustSelector(TrustSelectorType type, Set<String> identities) {
|
||||||
|
/** Validates and snapshots finite identities. */
|
||||||
|
public TrustSelector {
|
||||||
|
Objects.requireNonNull(type, "type"); identities = Set.copyOf(Objects.requireNonNull(identities));
|
||||||
|
if (identities.isEmpty() || identities.size() > 64) {
|
||||||
|
throw new IllegalArgumentException("OCSP requester trust selector is empty or oversized");
|
||||||
|
}
|
||||||
|
for (String identity : identities) {
|
||||||
|
if (type == TrustSelectorType.EXACT_REQUESTER_CERTIFICATE_COMMITMENTS) requireDigest(identity);
|
||||||
|
else new PkiId(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One exact active request-signature binding reference. */
|
||||||
|
public record RequestSignatureBinding(String bindingId, String commitment) {
|
||||||
|
/** Validates stable binding identity and commitment. */
|
||||||
|
public RequestSignatureBinding {
|
||||||
|
Permission.requireId(bindingId, "OCSP request-signature binding");
|
||||||
|
Permission.requireBounded(commitment, 512, "OCSP request-signature commitment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One cryptographic requester-certificate mapping. */
|
||||||
|
public record PrincipalMapping(String mappingId, String principalId, PrincipalMappingType type,
|
||||||
|
String commitment) {
|
||||||
|
/** Validates stable mapping metadata and fixed commitment. */
|
||||||
|
public PrincipalMapping {
|
||||||
|
Permission.requireId(mappingId, "OCSP requester mapping"); Permission.requirePrincipal(principalId);
|
||||||
|
Objects.requireNonNull(type, "type"); requireDigest(commitment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finite immutable requester trust, mapping, signature and chain policy. */
|
||||||
|
public record RequesterTrustPolicy(List<TrustSelector> trustSelectors,
|
||||||
|
List<PrincipalMapping> principalMappings, List<RequestSignatureBinding> signatureBindings,
|
||||||
|
RequesterRevocationPolicy revocationPolicy, int maximumSignerCertificates,
|
||||||
|
int maximumSignerChainBytes) {
|
||||||
|
/** Validates and snapshots the complete closed policy. */
|
||||||
|
public RequesterTrustPolicy {
|
||||||
|
trustSelectors = List.copyOf(Objects.requireNonNull(trustSelectors));
|
||||||
|
principalMappings = List.copyOf(Objects.requireNonNull(principalMappings));
|
||||||
|
signatureBindings = List.copyOf(Objects.requireNonNull(signatureBindings));
|
||||||
|
Objects.requireNonNull(revocationPolicy, "revocationPolicy");
|
||||||
|
if (trustSelectors.isEmpty() || trustSelectors.size() > 16 || principalMappings.isEmpty()
|
||||||
|
|| principalMappings.size() > 256 || signatureBindings.isEmpty() || signatureBindings.size() > 16
|
||||||
|
|| maximumSignerCertificates < 1 || maximumSignerCertificates > 16
|
||||||
|
|| maximumSignerChainBytes < 1024 || maximumSignerChainBytes > 1_048_576) {
|
||||||
|
throw new IllegalArgumentException("OCSP requester trust policy bounds are invalid");
|
||||||
|
}
|
||||||
|
if (principalMappings.stream().map(PrincipalMapping::mappingId).distinct().count()
|
||||||
|
!= principalMappings.size()
|
||||||
|
|| signatureBindings.stream().map(RequestSignatureBinding::bindingId).distinct().count()
|
||||||
|
!= signatureBindings.size()) {
|
||||||
|
throw new IllegalArgumentException("OCSP requester policy identities are duplicated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/** Exact signing authority. */
|
/** Exact signing authority. */
|
||||||
public enum SigningMode {
|
public enum SigningMode {
|
||||||
ISSUER_SIGNED(1), DELEGATED_RESPONDER(2);
|
ISSUER_SIGNED(1), DELEGATED_RESPONDER(2);
|
||||||
@@ -115,7 +234,8 @@ public final class OcspResponderService {
|
|||||||
String signatureAlgorithm, Optional<String> signatureBindingId, String signatureBindingCommitment,
|
String signatureAlgorithm, Optional<String> signatureBindingId, String signatureBindingCommitment,
|
||||||
OcspResponseService.ResponderId responderIdForm, Duration responseValidity, NoncePolicy noncePolicy,
|
OcspResponseService.ResponderId responderIdForm, Duration responseValidity, NoncePolicy noncePolicy,
|
||||||
int maximumNonceBytes, Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumRequestBytes,
|
int maximumNonceBytes, Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumRequestBytes,
|
||||||
int maximumEntries, Duration cacheLifetime) {
|
int maximumEntries, Duration cacheLifetime, RequestAuthenticationMode requestAuthenticationMode,
|
||||||
|
Optional<RequesterTrustPolicy> requesterTrustPolicy) {
|
||||||
/** Snapshots the typed finite registration input. */
|
/** Snapshots the typed finite registration input. */
|
||||||
public Registration {
|
public Registration {
|
||||||
Permission.requireId(responderId, "OCSP responder"); Permission.requireId(alias, "OCSP alias");
|
Permission.requireId(responderId, "OCSP responder"); Permission.requireId(alias, "OCSP alias");
|
||||||
@@ -128,6 +248,22 @@ public final class OcspResponderService {
|
|||||||
Objects.requireNonNull(responseValidity); Objects.requireNonNull(noncePolicy);
|
Objects.requireNonNull(responseValidity); Objects.requireNonNull(noncePolicy);
|
||||||
acceptedHashes = Set.copyOf(Objects.requireNonNull(acceptedHashes));
|
acceptedHashes = Set.copyOf(Objects.requireNonNull(acceptedHashes));
|
||||||
Objects.requireNonNull(cacheLifetime);
|
Objects.requireNonNull(cacheLifetime);
|
||||||
|
Objects.requireNonNull(requestAuthenticationMode, "requestAuthenticationMode");
|
||||||
|
requesterTrustPolicy = Objects.requireNonNull(requesterTrustPolicy, "requesterTrustPolicy");
|
||||||
|
requirePolicyShape(requestAuthenticationMode, requesterTrustPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates the default public unsigned-only registration. */
|
||||||
|
public Registration(String responderId, String alias, PkiId authorityId, PkiId issuerId,
|
||||||
|
SigningMode signingMode, PkiId responderCredentialId, KeyRef signingKeyRef, PkiId chainPathId,
|
||||||
|
String signatureAlgorithm, Optional<String> signatureBindingId, String signatureBindingCommitment,
|
||||||
|
OcspResponseService.ResponderId responderIdForm, Duration responseValidity, NoncePolicy noncePolicy,
|
||||||
|
int maximumNonceBytes, Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumRequestBytes,
|
||||||
|
int maximumEntries, Duration cacheLifetime) {
|
||||||
|
this(responderId, alias, authorityId, issuerId, signingMode, responderCredentialId, signingKeyRef,
|
||||||
|
chainPathId, signatureAlgorithm, signatureBindingId, signatureBindingCommitment, responderIdForm,
|
||||||
|
responseValidity, noncePolicy, maximumNonceBytes, acceptedHashes, maximumRequestBytes,
|
||||||
|
maximumEntries, cacheLifetime, RequestAuthenticationMode.UNSIGNED_ONLY, Optional.empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +274,8 @@ public final class OcspResponderService {
|
|||||||
String signatureBindingCommitment, OcspResponseService.ResponderId responderIdForm,
|
String signatureBindingCommitment, OcspResponseService.ResponderId responderIdForm,
|
||||||
Duration responseValidity, NoncePolicy noncePolicy, int maximumNonceBytes,
|
Duration responseValidity, NoncePolicy noncePolicy, int maximumNonceBytes,
|
||||||
Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumRequestBytes,
|
Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumRequestBytes,
|
||||||
int maximumEntries, Duration cacheLifetime, State state, Instant createdAt,
|
int maximumEntries, Duration cacheLifetime, RequestAuthenticationMode requestAuthenticationMode,
|
||||||
|
Optional<RequesterTrustPolicy> requesterTrustPolicy, State state, Instant createdAt,
|
||||||
String configurationCommitment) {
|
String configurationCommitment) {
|
||||||
/** Validates all finite immutable dependencies and the record commitment. */
|
/** Validates all finite immutable dependencies and the record commitment. */
|
||||||
public Responder {
|
public Responder {
|
||||||
@@ -157,6 +294,9 @@ public final class OcspResponderService {
|
|||||||
acceptedHashes = Set.copyOf(Objects.requireNonNull(acceptedHashes));
|
acceptedHashes = Set.copyOf(Objects.requireNonNull(acceptedHashes));
|
||||||
if (acceptedHashes.isEmpty()) throw new IllegalArgumentException("OCSP CertID hashes are empty");
|
if (acceptedHashes.isEmpty()) throw new IllegalArgumentException("OCSP CertID hashes are empty");
|
||||||
positive(cacheLifetime, responseValidity, "cache lifetime"); Objects.requireNonNull(state);
|
positive(cacheLifetime, responseValidity, "cache lifetime"); Objects.requireNonNull(state);
|
||||||
|
Objects.requireNonNull(requestAuthenticationMode, "requestAuthenticationMode");
|
||||||
|
requesterTrustPolicy = Objects.requireNonNull(requesterTrustPolicy, "requesterTrustPolicy");
|
||||||
|
requirePolicyShape(requestAuthenticationMode, requesterTrustPolicy);
|
||||||
Objects.requireNonNull(createdAt); requireDigest(configurationCommitment);
|
Objects.requireNonNull(createdAt); requireDigest(configurationCommitment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +307,7 @@ public final class OcspResponderService {
|
|||||||
private final X509AlgorithmBindingRegistry bindings;
|
private final X509AlgorithmBindingRegistry bindings;
|
||||||
private final Optional<OcspResponseService> signing;
|
private final Optional<OcspResponseService> signing;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
private final Map<String, Responder> aliasIndex = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Binds responder control to one realm and one authoritative PKI repository. */
|
/** Binds responder control to one realm and one authoritative PKI repository. */
|
||||||
public OcspResponderService(RealmId realmId, ServerControlStore control, PkiRepository repository,
|
public OcspResponderService(RealmId realmId, ServerControlStore control, PkiRepository repository,
|
||||||
@@ -185,7 +326,8 @@ public final class OcspResponderService {
|
|||||||
supplied.chainPathId(), supplied.signatureAlgorithm(), supplied.signatureBindingId(),
|
supplied.chainPathId(), supplied.signatureAlgorithm(), supplied.signatureBindingId(),
|
||||||
supplied.signatureBindingCommitment(), supplied.responderIdForm(), supplied.responseValidity(),
|
supplied.signatureBindingCommitment(), supplied.responderIdForm(), supplied.responseValidity(),
|
||||||
supplied.noncePolicy(), supplied.maximumNonceBytes(), supplied.acceptedHashes(),
|
supplied.noncePolicy(), supplied.maximumNonceBytes(), supplied.acceptedHashes(),
|
||||||
supplied.maximumRequestBytes(), supplied.maximumEntries(), supplied.cacheLifetime()));
|
supplied.maximumRequestBytes(), supplied.maximumEntries(), supplied.cacheLifetime(),
|
||||||
|
supplied.requestAuthenticationMode(), supplied.requesterTrustPolicy()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Canonically seals an already constructed internal draft. */
|
/** Canonically seals an already constructed internal draft. */
|
||||||
@@ -196,12 +338,13 @@ public final class OcspResponderService {
|
|||||||
if (!supplied.configurationCommitment().equals(commitment(supplied, true))) {
|
if (!supplied.configurationCommitment().equals(commitment(supplied, true))) {
|
||||||
throw new IllegalArgumentException("OCSP responder commitment differs");
|
throw new IllegalArgumentException("OCSP responder commitment differs");
|
||||||
}
|
}
|
||||||
if (!aliases(supplied.alias()).isEmpty()) {
|
if (aliasIndex.containsKey(supplied.alias())) {
|
||||||
throw new IllegalStateException("OCSP responder alias already exists");
|
throw new IllegalStateException("OCSP responder alias already exists");
|
||||||
}
|
}
|
||||||
validateDependencies(supplied, true);
|
validateDependencies(supplied, true);
|
||||||
Responder sealed = seal(supplied, State.INACTIVE);
|
Responder sealed = seal(supplied, State.INACTIVE);
|
||||||
control.mutateProtocol(List.of(new ServerControlStore.ProtocolMutation(record(sealed), Optional.empty())));
|
control.mutateProtocol(List.of(new ServerControlStore.ProtocolMutation(record(sealed), Optional.empty())));
|
||||||
|
aliasIndex.put(sealed.alias(), sealed);
|
||||||
return sealed;
|
return sealed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +360,15 @@ public final class OcspResponderService {
|
|||||||
if (result.state() != State.ACTIVE) {
|
if (result.state() != State.ACTIVE) {
|
||||||
throw new IllegalStateException("OCSP responder is inactive");
|
throw new IllegalStateException("OCSP responder is inactive");
|
||||||
}
|
}
|
||||||
validateDependencies(result, false); return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves one unique durable alias regardless of activation state. */
|
/** Resolves one unique durable alias regardless of activation state. */
|
||||||
public Responder requireAlias(String alias) {
|
public Responder requireAlias(String alias) {
|
||||||
Permission.requireId(alias, "OCSP alias");
|
Permission.requireId(alias, "OCSP alias");
|
||||||
List<Responder> matches = aliases(alias);
|
Responder indexed = aliasIndex.get(alias);
|
||||||
if (matches.size() != 1) throw new IllegalArgumentException("OCSP responder alias is unavailable");
|
if (indexed == null) throw new IllegalArgumentException("OCSP responder alias is unavailable");
|
||||||
return matches.getFirst();
|
return indexed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lists one bounded deterministic responder page. */
|
/** Lists one bounded deterministic responder page. */
|
||||||
@@ -236,33 +379,31 @@ public final class OcspResponderService {
|
|||||||
page.nextOffset(), page.hasMore());
|
page.nextOffset(), page.hasMore());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Responder> aliases(String alias) {
|
|
||||||
List<Responder> matches = new ArrayList<>();
|
|
||||||
int offset = 0;
|
|
||||||
while (true) {
|
|
||||||
ServerControlStore.Page<Responder> page = list(offset, 256);
|
|
||||||
page.values().stream().filter(value -> value.alias().equals(alias)).forEach(matches::add);
|
|
||||||
if (!page.hasMore()) return List.copyOf(matches);
|
|
||||||
offset = page.nextOffset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Atomically changes only responder activation after dependency revalidation. */
|
/** Atomically changes only responder activation after dependency revalidation. */
|
||||||
public Responder setActive(String responderId, boolean active) {
|
public Responder setActive(String responderId, boolean active) {
|
||||||
Responder prior = require(responderId); validateDependencies(prior, true);
|
Responder prior = require(responderId); validateDependencies(prior, true);
|
||||||
Responder next = seal(prior, active ? State.ACTIVE : State.INACTIVE);
|
Responder next = seal(prior, active ? State.ACTIVE : State.INACTIVE);
|
||||||
control.mutateProtocol(List.of(new ServerControlStore.ProtocolMutation(record(next),
|
control.mutateProtocol(List.of(new ServerControlStore.ProtocolMutation(record(next),
|
||||||
Optional.of(record(prior).commitment()))));
|
Optional.of(record(prior).commitment()))));
|
||||||
|
aliasIndex.put(next.alias(), next);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Revalidates every durable binding during realm recovery. */
|
/** Revalidates every durable binding during realm recovery. */
|
||||||
public void validateAll() {
|
public void validateAll() {
|
||||||
|
Map<String, Responder> recovered = new java.util.HashMap<>();
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
ServerControlStore.Page<Responder> page = list(offset, 256);
|
ServerControlStore.Page<Responder> page = list(offset, 256);
|
||||||
page.values().forEach(value -> validateDependencies(value, true));
|
page.values().forEach(value -> {
|
||||||
if (!page.hasMore()) return;
|
validateDependencies(value, true);
|
||||||
|
if (recovered.putIfAbsent(value.alias(), value) != null) {
|
||||||
|
throw new IllegalStateException("OCSP responder alias is duplicated");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!page.hasMore()) {
|
||||||
|
aliasIndex.clear(); aliasIndex.putAll(recovered); return;
|
||||||
|
}
|
||||||
offset = page.nextOffset();
|
offset = page.nextOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,17 +415,32 @@ public final class OcspResponderService {
|
|||||||
OcspResponseService.ResponderId responderIdForm, Duration validity, NoncePolicy noncePolicy,
|
OcspResponseService.ResponderId responderIdForm, Duration validity, NoncePolicy noncePolicy,
|
||||||
int maximumNonceBytes, Set<OcspResponseService.CertIdHash> hashes, int maximumRequestBytes,
|
int maximumNonceBytes, Set<OcspResponseService.CertIdHash> hashes, int maximumRequestBytes,
|
||||||
int maximumEntries, Duration cacheLifetime) {
|
int maximumEntries, Duration cacheLifetime) {
|
||||||
|
return create(responderId, alias, authorityId, issuerId, signingMode, responderCredentialId,
|
||||||
|
signingKeyRef, chainPathId, signatureAlgorithm, signatureBindingId, bindingCommitment,
|
||||||
|
responderIdForm, validity, noncePolicy, maximumNonceBytes, hashes, maximumRequestBytes,
|
||||||
|
maximumEntries, cacheLifetime, RequestAuthenticationMode.UNSIGNED_ONLY, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a canonical unsealed record with explicit signed-request policy. */
|
||||||
|
public Responder create(String responderId, String alias, PkiId authorityId, PkiId issuerId,
|
||||||
|
SigningMode signingMode, PkiId responderCredentialId, KeyRef signingKeyRef, PkiId chainPathId,
|
||||||
|
String signatureAlgorithm, Optional<String> signatureBindingId, String bindingCommitment,
|
||||||
|
OcspResponseService.ResponderId responderIdForm, Duration validity, NoncePolicy noncePolicy,
|
||||||
|
int maximumNonceBytes, Set<OcspResponseService.CertIdHash> hashes, int maximumRequestBytes,
|
||||||
|
int maximumEntries, Duration cacheLifetime, RequestAuthenticationMode requestAuthenticationMode,
|
||||||
|
Optional<RequesterTrustPolicy> requesterTrustPolicy) {
|
||||||
Responder draft = new Responder(responderId, alias, realmId, authorityId, issuerId, signingMode,
|
Responder draft = new Responder(responderId, alias, realmId, authorityId, issuerId, signingMode,
|
||||||
responderCredentialId, signingKeyRef, chainPathId, signatureAlgorithm, signatureBindingId,
|
responderCredentialId, signingKeyRef, chainPathId, signatureAlgorithm, signatureBindingId,
|
||||||
bindingCommitment, responderIdForm, validity, noncePolicy, maximumNonceBytes, hashes,
|
bindingCommitment, responderIdForm, validity, noncePolicy, maximumNonceBytes, hashes,
|
||||||
maximumRequestBytes, maximumEntries, cacheLifetime, State.INACTIVE, clock.instant(),
|
maximumRequestBytes, maximumEntries, cacheLifetime, requestAuthenticationMode,
|
||||||
"0".repeat(64));
|
requesterTrustPolicy, State.INACTIVE, clock.instant(), "0".repeat(64));
|
||||||
return new Responder(draft.responderId(), draft.alias(), draft.realmId(), draft.authorityId(), draft.issuerId(),
|
return new Responder(draft.responderId(), draft.alias(), draft.realmId(), draft.authorityId(), draft.issuerId(),
|
||||||
draft.signingMode(), draft.responderCredentialId(), draft.signingKeyRef(), draft.chainPathId(),
|
draft.signingMode(), draft.responderCredentialId(), draft.signingKeyRef(), draft.chainPathId(),
|
||||||
draft.signatureAlgorithm(), draft.signatureBindingId(), draft.signatureBindingCommitment(),
|
draft.signatureAlgorithm(), draft.signatureBindingId(), draft.signatureBindingCommitment(),
|
||||||
draft.responderIdForm(), draft.responseValidity(), draft.noncePolicy(), draft.maximumNonceBytes(),
|
draft.responderIdForm(), draft.responseValidity(), draft.noncePolicy(), draft.maximumNonceBytes(),
|
||||||
draft.acceptedHashes(), draft.maximumRequestBytes(), draft.maximumEntries(), draft.cacheLifetime(),
|
draft.acceptedHashes(), draft.maximumRequestBytes(), draft.maximumEntries(), draft.cacheLifetime(),
|
||||||
draft.state(), draft.createdAt(), commitment(draft, true));
|
draft.requestAuthenticationMode(), draft.requesterTrustPolicy(), draft.state(), draft.createdAt(),
|
||||||
|
commitment(draft, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Responder seal(Responder value, State state) {
|
private Responder seal(Responder value, State state) {
|
||||||
@@ -293,13 +449,15 @@ public final class OcspResponderService {
|
|||||||
value.chainPathId(), value.signatureAlgorithm(), value.signatureBindingId(),
|
value.chainPathId(), value.signatureAlgorithm(), value.signatureBindingId(),
|
||||||
value.signatureBindingCommitment(), value.responderIdForm(), value.responseValidity(),
|
value.signatureBindingCommitment(), value.responderIdForm(), value.responseValidity(),
|
||||||
value.noncePolicy(), value.maximumNonceBytes(), value.acceptedHashes(), value.maximumRequestBytes(),
|
value.noncePolicy(), value.maximumNonceBytes(), value.acceptedHashes(), value.maximumRequestBytes(),
|
||||||
value.maximumEntries(), value.cacheLifetime(), state, value.createdAt(), value.configurationCommitment());
|
value.maximumEntries(), value.cacheLifetime(), value.requestAuthenticationMode(),
|
||||||
|
value.requesterTrustPolicy(), state, value.createdAt(), value.configurationCommitment());
|
||||||
return new Responder(draft.responderId(), draft.alias(), draft.realmId(), draft.authorityId(), draft.issuerId(),
|
return new Responder(draft.responderId(), draft.alias(), draft.realmId(), draft.authorityId(), draft.issuerId(),
|
||||||
draft.signingMode(), draft.responderCredentialId(), draft.signingKeyRef(), draft.chainPathId(),
|
draft.signingMode(), draft.responderCredentialId(), draft.signingKeyRef(), draft.chainPathId(),
|
||||||
draft.signatureAlgorithm(), draft.signatureBindingId(), draft.signatureBindingCommitment(),
|
draft.signatureAlgorithm(), draft.signatureBindingId(), draft.signatureBindingCommitment(),
|
||||||
draft.responderIdForm(), draft.responseValidity(), draft.noncePolicy(), draft.maximumNonceBytes(),
|
draft.responderIdForm(), draft.responseValidity(), draft.noncePolicy(), draft.maximumNonceBytes(),
|
||||||
draft.acceptedHashes(), draft.maximumRequestBytes(), draft.maximumEntries(), draft.cacheLifetime(),
|
draft.acceptedHashes(), draft.maximumRequestBytes(), draft.maximumEntries(), draft.cacheLifetime(),
|
||||||
draft.state(), draft.createdAt(), commitment(draft, true));
|
draft.requestAuthenticationMode(), draft.requesterTrustPolicy(), draft.state(), draft.createdAt(),
|
||||||
|
commitment(draft, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateDependencies(Responder value, boolean proveSigning) {
|
private void validateDependencies(Responder value, boolean proveSigning) {
|
||||||
@@ -310,6 +468,26 @@ public final class OcspResponderService {
|
|||||||
throw new IllegalStateException("OCSP algorithm registry commitment differs");
|
throw new IllegalStateException("OCSP algorithm registry commitment differs");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
value.requesterTrustPolicy().ifPresent(policy -> {
|
||||||
|
for (RequestSignatureBinding reference : policy.signatureBindings()) {
|
||||||
|
X509AlgorithmBinding binding = bindings.require(reference.bindingId(), reference.commitment());
|
||||||
|
if (binding.role() != X509AlgorithmBinding.Role.OCSP_REQUEST_SIGNATURE) {
|
||||||
|
throw new IllegalStateException("OCSP requester signature binding has the wrong role");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (TrustSelector selector : policy.trustSelectors()) {
|
||||||
|
if (selector.type() == TrustSelectorType.MANAGED_ISSUER_GENERATIONS) {
|
||||||
|
selector.identities().forEach(identity -> repository.issuer(new PkiId(identity)).orElseThrow(
|
||||||
|
() -> new IllegalStateException("OCSP requester issuer trust is unavailable")));
|
||||||
|
} else if (selector.type() == TrustSelectorType.CONFIGURED_TRUST_ANCHORS) {
|
||||||
|
selector.identities().forEach(identity -> {
|
||||||
|
try { certificate(new PkiId(identity)); }
|
||||||
|
catch (IOException failure) { throw new IllegalStateException("OCSP requester trust anchor is unavailable"); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
policy.principalMappings().forEach(mapping -> control.requirePrincipal(mapping.principalId()));
|
||||||
|
});
|
||||||
IssuerGeneration issuer = repository.issuer(value.issuerId()).orElseThrow();
|
IssuerGeneration issuer = repository.issuer(value.issuerId()).orElseThrow();
|
||||||
IssuerChainPath path = repository.chainPath(value.chainPathId()).orElseThrow();
|
IssuerChainPath path = repository.chainPath(value.chainPathId()).orElseThrow();
|
||||||
if (!issuer.authorityId().equals(value.authorityId()) || !path.authorityId().equals(value.authorityId())
|
if (!issuer.authorityId().equals(value.authorityId()) || !path.authorityId().equals(value.authorityId())
|
||||||
@@ -399,11 +577,25 @@ public final class OcspResponderService {
|
|||||||
return sha256(encode(value, ignoreStored));
|
return sha256(encode(value, ignoreStored));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a safe deterministic commitment to one request-authentication policy. */
|
||||||
|
public static String requestPolicyCommitment(Registration value) {
|
||||||
|
Objects.requireNonNull(value, "value");
|
||||||
|
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(512);
|
||||||
|
DataOutputStream output = new DataOutputStream(bytes)) {
|
||||||
|
output.writeInt(value.requestAuthenticationMode().code());
|
||||||
|
output.writeBoolean(value.requesterTrustPolicy().isPresent());
|
||||||
|
if (value.requesterTrustPolicy().isPresent()) writePolicy(output, value.requesterTrustPolicy().orElseThrow());
|
||||||
|
output.flush(); return sha256(bytes.toByteArray());
|
||||||
|
} catch (IOException impossible) {
|
||||||
|
throw new IllegalStateException("OCSP requester policy encoding failed", impossible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] encode(Responder value) { return encode(value, false); }
|
private static byte[] encode(Responder value) { return encode(value, false); }
|
||||||
private static byte[] encode(Responder value, boolean ignoreCommitment) {
|
private static byte[] encode(Responder value, boolean ignoreCommitment) {
|
||||||
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(1024);
|
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(1024);
|
||||||
DataOutputStream output = new DataOutputStream(bytes)) {
|
DataOutputStream output = new DataOutputStream(bytes)) {
|
||||||
output.writeInt(1); write(output, value.responderId()); write(output, value.alias());
|
output.writeInt(2); write(output, value.responderId()); write(output, value.alias());
|
||||||
write(output, value.realmId().value()); write(output, value.authorityId().value());
|
write(output, value.realmId().value()); write(output, value.authorityId().value());
|
||||||
write(output, value.issuerId().value()); output.writeInt(value.signingMode().code());
|
write(output, value.issuerId().value()); output.writeInt(value.signingMode().code());
|
||||||
write(output, value.responderCredentialId().value()); write(output, value.signingKeyRef().value());
|
write(output, value.responderCredentialId().value()); write(output, value.signingKeyRef().value());
|
||||||
@@ -417,7 +609,11 @@ public final class OcspResponderService {
|
|||||||
output.writeInt(hash.code());
|
output.writeInt(hash.code());
|
||||||
}
|
}
|
||||||
output.writeInt(value.maximumRequestBytes()); output.writeInt(value.maximumEntries());
|
output.writeInt(value.maximumRequestBytes()); output.writeInt(value.maximumEntries());
|
||||||
output.writeLong(value.cacheLifetime().toSeconds()); output.writeInt(value.state().code());
|
output.writeLong(value.cacheLifetime().toSeconds());
|
||||||
|
output.writeInt(value.requestAuthenticationMode().code());
|
||||||
|
output.writeBoolean(value.requesterTrustPolicy().isPresent());
|
||||||
|
if (value.requesterTrustPolicy().isPresent()) writePolicy(output, value.requesterTrustPolicy().orElseThrow());
|
||||||
|
output.writeInt(value.state().code());
|
||||||
output.writeLong(value.createdAt().toEpochMilli());
|
output.writeLong(value.createdAt().toEpochMilli());
|
||||||
write(output, ignoreCommitment ? "0".repeat(64) : value.configurationCommitment()); output.flush();
|
write(output, ignoreCommitment ? "0".repeat(64) : value.configurationCommitment()); output.flush();
|
||||||
return bytes.toByteArray();
|
return bytes.toByteArray();
|
||||||
@@ -427,7 +623,7 @@ public final class OcspResponderService {
|
|||||||
private static Responder decode(byte[] payload) {
|
private static Responder decode(byte[] payload) {
|
||||||
if (payload.length == 0 || payload.length > 65_536) throw new IllegalStateException("OCSP record bound differs");
|
if (payload.length == 0 || payload.length > 65_536) throw new IllegalStateException("OCSP record bound differs");
|
||||||
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload))) {
|
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload))) {
|
||||||
if (input.readInt() != 1) throw new IOException("OCSP record schema is obsolete");
|
if (input.readInt() != 2) throw new IOException("OCSP record schema is obsolete");
|
||||||
String id = read(input); String alias = read(input); RealmId realm = new RealmId(read(input));
|
String id = read(input); String alias = read(input); RealmId realm = new RealmId(read(input));
|
||||||
PkiId authority = new PkiId(read(input)); PkiId issuer = new PkiId(read(input));
|
PkiId authority = new PkiId(read(input)); PkiId issuer = new PkiId(read(input));
|
||||||
SigningMode mode = SigningMode.fromCode(input.readInt()); PkiId credential = new PkiId(read(input));
|
SigningMode mode = SigningMode.fromCode(input.readInt()); PkiId credential = new PkiId(read(input));
|
||||||
@@ -443,11 +639,14 @@ public final class OcspResponderService {
|
|||||||
hashes.add(OcspResponseService.CertIdHash.fromCode(input.readInt()));
|
hashes.add(OcspResponseService.CertIdHash.fromCode(input.readInt()));
|
||||||
}
|
}
|
||||||
int requestBytes = input.readInt(); int entries = input.readInt(); Duration cache = Duration.ofSeconds(input.readLong());
|
int requestBytes = input.readInt(); int entries = input.readInt(); Duration cache = Duration.ofSeconds(input.readLong());
|
||||||
|
RequestAuthenticationMode authenticationMode = RequestAuthenticationMode.fromCode(input.readInt());
|
||||||
|
Optional<RequesterTrustPolicy> trustPolicy = input.readBoolean()
|
||||||
|
? Optional.of(readPolicy(input)) : Optional.empty();
|
||||||
State state = State.fromCode(input.readInt()); Instant created = Instant.ofEpochMilli(input.readLong());
|
State state = State.fromCode(input.readInt()); Instant created = Instant.ofEpochMilli(input.readLong());
|
||||||
String commitment = read(input); if (input.read() != -1) throw new IOException("Trailing OCSP record data");
|
String commitment = read(input); if (input.read() != -1) throw new IOException("Trailing OCSP record data");
|
||||||
return new Responder(id, alias, realm, authority, issuer, mode, credential, key, path, algorithm,
|
return new Responder(id, alias, realm, authority, issuer, mode, credential, key, path, algorithm,
|
||||||
binding, bindingCommitment, form, validity, nonce, nonceBytes, hashes, requestBytes,
|
binding, bindingCommitment, form, validity, nonce, nonceBytes, hashes, requestBytes,
|
||||||
entries, cache, state, created, commitment);
|
entries, cache, authenticationMode, trustPolicy, state, created, commitment);
|
||||||
} catch (IOException | RuntimeException failure) {
|
} catch (IOException | RuntimeException failure) {
|
||||||
throw new IllegalStateException("OCSP responder record is invalid");
|
throw new IllegalStateException("OCSP responder record is invalid");
|
||||||
}
|
}
|
||||||
@@ -458,6 +657,60 @@ public final class OcspResponderService {
|
|||||||
if (encoded.length > 16_384) throw new IOException("OCSP string bound differs");
|
if (encoded.length > 16_384) throw new IOException("OCSP string bound differs");
|
||||||
output.writeInt(encoded.length); output.write(encoded);
|
output.writeInt(encoded.length); output.write(encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void writePolicy(DataOutputStream output, RequesterTrustPolicy policy) throws IOException {
|
||||||
|
output.writeInt(policy.trustSelectors().size());
|
||||||
|
for (TrustSelector selector : policy.trustSelectors()) {
|
||||||
|
output.writeInt(selector.type().code()); output.writeInt(selector.identities().size());
|
||||||
|
for (String identity : selector.identities().stream().sorted().toList()) write(output, identity);
|
||||||
|
}
|
||||||
|
output.writeInt(policy.principalMappings().size());
|
||||||
|
for (PrincipalMapping mapping : policy.principalMappings().stream()
|
||||||
|
.sorted(java.util.Comparator.comparing(PrincipalMapping::mappingId)).toList()) {
|
||||||
|
write(output, mapping.mappingId()); write(output, mapping.principalId());
|
||||||
|
output.writeInt(mapping.type().code()); write(output, mapping.commitment());
|
||||||
|
}
|
||||||
|
output.writeInt(policy.signatureBindings().size());
|
||||||
|
for (RequestSignatureBinding binding : policy.signatureBindings().stream()
|
||||||
|
.sorted(java.util.Comparator.comparing(RequestSignatureBinding::bindingId)).toList()) {
|
||||||
|
write(output, binding.bindingId()); write(output, binding.commitment());
|
||||||
|
}
|
||||||
|
output.writeInt(policy.revocationPolicy().code()); output.writeInt(policy.maximumSignerCertificates());
|
||||||
|
output.writeInt(policy.maximumSignerChainBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
|
private static RequesterTrustPolicy readPolicy(DataInputStream input) throws IOException {
|
||||||
|
int selectorCount = boundedCount(input.readInt(), 16, "trust selector");
|
||||||
|
List<TrustSelector> selectors = new ArrayList<>(selectorCount);
|
||||||
|
for (int index = 0; index < selectorCount; index++) {
|
||||||
|
TrustSelectorType type = TrustSelectorType.fromCode(input.readInt());
|
||||||
|
int identityCount = boundedCount(input.readInt(), 64, "trust identity");
|
||||||
|
Set<String> identities = new java.util.HashSet<>();
|
||||||
|
for (int identity = 0; identity < identityCount; identity++) {
|
||||||
|
if (!identities.add(read(input))) throw new IOException("Duplicate OCSP trust identity");
|
||||||
|
}
|
||||||
|
selectors.add(new TrustSelector(type, identities));
|
||||||
|
}
|
||||||
|
int mappingCount = boundedCount(input.readInt(), 256, "principal mapping");
|
||||||
|
List<PrincipalMapping> mappings = new ArrayList<>(mappingCount);
|
||||||
|
for (int index = 0; index < mappingCount; index++) {
|
||||||
|
mappings.add(new PrincipalMapping(read(input), read(input),
|
||||||
|
PrincipalMappingType.fromCode(input.readInt()), read(input)));
|
||||||
|
}
|
||||||
|
int bindingCount = boundedCount(input.readInt(), 16, "signature binding");
|
||||||
|
List<RequestSignatureBinding> bindings = new ArrayList<>(bindingCount);
|
||||||
|
for (int index = 0; index < bindingCount; index++) {
|
||||||
|
bindings.add(new RequestSignatureBinding(read(input), read(input)));
|
||||||
|
}
|
||||||
|
return new RequesterTrustPolicy(selectors, mappings, bindings,
|
||||||
|
RequesterRevocationPolicy.fromCode(input.readInt()), input.readInt(), input.readInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int boundedCount(int value, int maximum, String label) throws IOException {
|
||||||
|
if (value < 1 || value > maximum) throw new IOException("OCSP " + label + " count differs");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
private static String read(DataInputStream input) throws IOException {
|
private static String read(DataInputStream input) throws IOException {
|
||||||
int length = input.readInt(); if (length < 0 || length > 16_384) throw new IOException("OCSP string bound differs");
|
int length = input.readInt(); if (length < 0 || length > 16_384) throw new IOException("OCSP string bound differs");
|
||||||
byte[] encoded = input.readNBytes(length); if (encoded.length != length) throw new IOException("OCSP record is truncated");
|
byte[] encoded = input.readNBytes(length); if (encoded.length != length) throw new IOException("OCSP record is truncated");
|
||||||
@@ -472,6 +725,13 @@ public final class OcspResponderService {
|
|||||||
private static void requireDigest(String value) {
|
private static void requireDigest(String value) {
|
||||||
if (value == null || !value.matches("[0-9a-f]{64}")) throw new IllegalArgumentException("OCSP commitment is invalid");
|
if (value == null || !value.matches("[0-9a-f]{64}")) throw new IllegalArgumentException("OCSP commitment is invalid");
|
||||||
}
|
}
|
||||||
|
private static void requirePolicyShape(RequestAuthenticationMode mode,
|
||||||
|
Optional<RequesterTrustPolicy> policy) {
|
||||||
|
if (mode == RequestAuthenticationMode.UNSIGNED_ONLY && policy.isPresent()
|
||||||
|
|| mode != RequestAuthenticationMode.UNSIGNED_ONLY && policy.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("OCSP requester trust policy does not match authentication mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
private static void positive(Duration value, Duration maximum, String name) {
|
private static void positive(Duration value, Duration maximum, String name) {
|
||||||
if (value == null || value.isZero() || value.isNegative() || value.compareTo(maximum) > 0) {
|
if (value == null || value.isZero() || value.isNegative() || value.compareTo(maximum) > 0) {
|
||||||
throw new IllegalArgumentException("OCSP " + name + " is invalid");
|
throw new IllegalArgumentException("OCSP " + name + " is invalid");
|
||||||
|
|||||||
@@ -434,7 +434,8 @@ public final class OperationSecurityDescriptors {
|
|||||||
+ atom(value.registration().responderId()) + ";authority="
|
+ atom(value.registration().responderId()) + ";authority="
|
||||||
+ atom(value.registration().authorityId().value()) + ";issuer="
|
+ atom(value.registration().authorityId().value()) + ";issuer="
|
||||||
+ atom(value.registration().issuerId().value()) + ";binding="
|
+ atom(value.registration().issuerId().value()) + ";binding="
|
||||||
+ value.registration().signatureBindingCommitment();
|
+ value.registration().signatureBindingCommitment() + ";requestPolicy="
|
||||||
|
+ OcspResponderService.requestPolicyCommitment(value.registration());
|
||||||
case ServerControlOperation.InspectOcspResponder value -> "responder=" + atom(value.responderId());
|
case ServerControlOperation.InspectOcspResponder value -> "responder=" + atom(value.responderId());
|
||||||
case ServerControlOperation.ListOcspResponders value -> "offset=" + value.offset()
|
case ServerControlOperation.ListOcspResponders value -> "offset=" + value.offset()
|
||||||
+ ";limit=" + value.limit();
|
+ ";limit=" + value.limit();
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ public final class Permission {
|
|||||||
CERTIFICATE_READ_PII(76), CERTIFICATE_DOWNLOAD(77), CERTIFICATE_PUBLICATION_CHANGE(78),
|
CERTIFICATE_READ_PII(76), CERTIFICATE_DOWNLOAD(77), CERTIFICATE_PUBLICATION_CHANGE(78),
|
||||||
CERTIFICATE_REVOKE(80), CERTIFICATE_HOLD(81), CERTIFICATE_RELEASE_HOLD(82),
|
CERTIFICATE_REVOKE(80), CERTIFICATE_HOLD(81), CERTIFICATE_RELEASE_HOLD(82),
|
||||||
REVOCATION_HISTORY_READ(83), CRL_GENERATE(84), CRL_PUBLISH(85), CRL_DOWNLOAD(86),
|
REVOCATION_HISTORY_READ(83), CRL_GENERATE(84), CRL_PUBLISH(85), CRL_DOWNLOAD(86),
|
||||||
OCSP_ADMINISTER(87), OCSP_RESPONDER_READ(88), PUBLICATION_REGISTER(100), PUBLICATION_READ(101),
|
OCSP_ADMINISTER(87), OCSP_RESPONDER_READ(88), OCSP_STATUS_QUERY(89),
|
||||||
|
PUBLICATION_REGISTER(100), PUBLICATION_READ(101),
|
||||||
PUBLICATION_PROCESS(102), PUBLICATION_RETRY(103), PUBLICATION_RECONCILE(104),
|
PUBLICATION_PROCESS(102), PUBLICATION_RETRY(103), PUBLICATION_RECONCILE(104),
|
||||||
AUDIT_READ_REDACTED(120), AUDIT_READ_FULL(121), AUDIT_READ_PII(122), AUDIT_EXPORT(123),
|
AUDIT_READ_REDACTED(120), AUDIT_READ_FULL(121), AUDIT_READ_PII(122), AUDIT_EXPORT(123),
|
||||||
AUDIT_INTEGRITY_VERIFY(124), BACKUP_EXPORT(140), BACKUP_VERIFY(141), RESTORE_EXECUTE(142),
|
AUDIT_INTEGRITY_VERIFY(124), BACKUP_EXPORT(140), BACKUP_VERIFY(141), RESTORE_EXECUTE(142),
|
||||||
@@ -181,17 +182,25 @@ public final class Permission {
|
|||||||
* @param profileId explicit profile when applicable
|
* @param profileId explicit profile when applicable
|
||||||
*/
|
*/
|
||||||
public record Scope(RealmId realmId, Optional<PkiId> authorityId, Optional<PkiId> issuerId,
|
public record Scope(RealmId realmId, Optional<PkiId> authorityId, Optional<PkiId> issuerId,
|
||||||
Optional<String> profileId) {
|
Optional<String> profileId, Optional<String> responderId) {
|
||||||
/** Validates the exact finite scope. */
|
/** Validates the exact finite scope. */
|
||||||
public Scope {
|
public Scope {
|
||||||
Objects.requireNonNull(realmId, "realmId");
|
Objects.requireNonNull(realmId, "realmId");
|
||||||
authorityId = Objects.requireNonNull(authorityId, "authorityId");
|
authorityId = Objects.requireNonNull(authorityId, "authorityId");
|
||||||
issuerId = Objects.requireNonNull(issuerId, "issuerId");
|
issuerId = Objects.requireNonNull(issuerId, "issuerId");
|
||||||
profileId = Objects.requireNonNull(profileId, "profileId").map(Permission::requireProfile);
|
profileId = Objects.requireNonNull(profileId, "profileId").map(Permission::requireProfile);
|
||||||
|
responderId = Objects.requireNonNull(responderId, "responderId")
|
||||||
|
.map(value -> { requireId(value, "OCSP responder"); return value; });
|
||||||
if (issuerId.isPresent() && authorityId.isEmpty()) {
|
if (issuerId.isPresent() && authorityId.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Issuer scope requires authority scope");
|
throw new IllegalArgumentException("Issuer scope requires authority scope");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a scope without an OCSP responder dimension. */
|
||||||
|
public Scope(RealmId realmId, Optional<PkiId> authorityId, Optional<PkiId> issuerId,
|
||||||
|
Optional<String> profileId) {
|
||||||
|
this(realmId, authorityId, issuerId, profileId, Optional.empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -419,6 +419,9 @@ public final class ServerControlOperationExecutor {
|
|||||||
"authorityId", text(value.authorityId().value()), "issuerId", text(value.issuerId().value()),
|
"authorityId", text(value.authorityId().value()), "issuerId", text(value.issuerId().value()),
|
||||||
"signingMode", text(value.signingMode().name()), "responderCredentialId",
|
"signingMode", text(value.signingMode().name()), "responderCredentialId",
|
||||||
text(value.responderCredentialId().value()), "state", text(value.state().name()),
|
text(value.responderCredentialId().value()), "state", text(value.state().name()),
|
||||||
|
"requestAuthenticationMode", text(value.requestAuthenticationMode().name()),
|
||||||
|
"requesterRevocationPolicy", text(value.requesterTrustPolicy()
|
||||||
|
.map(policy -> policy.revocationPolicy().name()).orElse("NOT_APPLICABLE")),
|
||||||
"configurationCommitment", text(value.configurationCommitment()));
|
"configurationCommitment", text(value.configurationCommitment()));
|
||||||
}
|
}
|
||||||
private static PkiOperationValue template(RoleTemplateCatalog.Template value) {
|
private static PkiOperationValue template(RoleTemplateCatalog.Template value) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ public final class ServerControlStore implements AutoCloseable {
|
|||||||
public static final String OCSP_RESPONDER = "io.zeroecho.server.ocsp-responder";
|
public static final String OCSP_RESPONDER = "io.zeroecho.server.ocsp-responder";
|
||||||
|
|
||||||
private static final int MAGIC = 0x5a455331;
|
private static final int MAGIC = 0x5a455331;
|
||||||
private static final int SCHEMA = 5;
|
private static final int SCHEMA = 6;
|
||||||
private static final int MAXIMUM_RECORD_BYTES = 1_048_576;
|
private static final int MAXIMUM_RECORD_BYTES = 1_048_576;
|
||||||
private static final int MAXIMUM_STRING_BYTES = 16_384;
|
private static final int MAXIMUM_STRING_BYTES = 16_384;
|
||||||
private static final int MAXIMUM_COLLECTION = 4_096;
|
private static final int MAXIMUM_COLLECTION = 4_096;
|
||||||
@@ -925,10 +925,12 @@ public final class ServerControlStore implements AutoCloseable {
|
|||||||
private static void writeScope(DataOutputStream out, Permission.Scope value) throws IOException {
|
private static void writeScope(DataOutputStream out, Permission.Scope value) throws IOException {
|
||||||
writeString(out, value.realmId().value()); writeOptionalPki(out, value.authorityId());
|
writeString(out, value.realmId().value()); writeOptionalPki(out, value.authorityId());
|
||||||
writeOptionalPki(out, value.issuerId()); writeOptionalString(out, value.profileId());
|
writeOptionalPki(out, value.issuerId()); writeOptionalString(out, value.profileId());
|
||||||
|
writeOptionalString(out, value.responderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Permission.Scope readScope(DataInputStream in) throws IOException {
|
private static Permission.Scope readScope(DataInputStream in) throws IOException {
|
||||||
return new Permission.Scope(new RealmId(readString(in)), readOptionalPki(in), readOptionalPki(in), readOptionalString(in));
|
return new Permission.Scope(new RealmId(readString(in)), readOptionalPki(in), readOptionalPki(in),
|
||||||
|
readOptionalString(in), readOptionalString(in));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writeString(DataOutputStream out, String value) throws IOException {
|
private static void writeString(DataOutputStream out, String value) throws IOException {
|
||||||
|
|||||||
@@ -211,6 +211,39 @@ public final class ServerRealmContext implements AutoCloseable {
|
|||||||
requireOpen();
|
requireOpen();
|
||||||
return control.requirePrincipal(principalId);
|
return control.requirePrincipal(principalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the protocol-only OCSP query permission for one exact responder
|
||||||
|
* and issuer generation. The signed-request identity remains independent of
|
||||||
|
* administrative and transport authentication.
|
||||||
|
*
|
||||||
|
* @param principalId cryptographically mapped requester principal
|
||||||
|
* @param responder exact active responder binding
|
||||||
|
* @return default-deny authorization decision
|
||||||
|
*/
|
||||||
|
public AuthorizationEngine.Decision authorizeOcspQuery(String principalId,
|
||||||
|
OcspResponderService.Responder responder) {
|
||||||
|
requireOpen();
|
||||||
|
SecurityPrincipal principal = control.requirePrincipal(principalId);
|
||||||
|
java.util.List<Permission.Grant> grants = new java.util.ArrayList<>(control.grantsFor(principalId));
|
||||||
|
for (RoleTemplateCatalog.Assignment assignment : control.assignmentsFor(principalId)) {
|
||||||
|
grants.addAll(roles.instantiate(assignment));
|
||||||
|
}
|
||||||
|
BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principalId);
|
||||||
|
grants.addAll(emergency.grants());
|
||||||
|
Permission.Scope scope = new Permission.Scope(configuration.realmId(),
|
||||||
|
Optional.of(responder.authorityId()), Optional.of(responder.issuerId()), Optional.empty(),
|
||||||
|
Optional.of(responder.responderId()));
|
||||||
|
Permission.Resource resource = new Permission.Resource(Permission.ResourceType.OCSP_RESPONDER,
|
||||||
|
scope, Optional.empty(), Optional.empty());
|
||||||
|
AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(
|
||||||
|
configuration.realmId(), configuration.authorityExposure(), principal,
|
||||||
|
Permission.Action.OCSP_STATUS_QUERY, resource, Permission.Relationship.ANY,
|
||||||
|
Permission.DataView.METADATA_REDACTED, Permission.Context.empty(), grants,
|
||||||
|
emergency.grantIds()));
|
||||||
|
if (decision.usedBreakGlass()) breakGlass.auditUse(principalId);
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Records one transport-safe lifecycle or request classification through the
|
* Records one transport-safe lifecycle or request classification through the
|
||||||
* shared realm audit authority.
|
* shared realm audit authority.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ package zeroecho.pki.server.http;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -243,9 +244,11 @@ final class HttpOperationCodec {
|
|||||||
}
|
}
|
||||||
case ServerControlOperation.CreateGrant.NAME -> {
|
case ServerControlOperation.CreateGrant.NAME -> {
|
||||||
fields.allowed(Set.of("grantId", "principalId", "effect", "action", "resourceType",
|
fields.allowed(Set.of("grantId", "principalId", "effect", "action", "resourceType",
|
||||||
"issuerId", "profileId", "relationship", "dataView", "conditions", "expiresAt"));
|
"issuerId", "profileId", "responderId", "relationship", "dataView", "conditions",
|
||||||
|
"expiresAt"));
|
||||||
Permission.Scope scope = new Permission.Scope(realmId, authority,
|
Permission.Scope scope = new Permission.Scope(realmId, authority,
|
||||||
fields.optionalText("issuerId").map(PkiId::new), fields.optionalText("profileId"));
|
fields.optionalText("issuerId").map(PkiId::new), fields.optionalText("profileId"),
|
||||||
|
fields.optionalText("responderId"));
|
||||||
yield new ServerControlOperation.CreateGrant(new Permission.Grant(fields.text("grantId"),
|
yield new ServerControlOperation.CreateGrant(new Permission.Grant(fields.text("grantId"),
|
||||||
fields.text("principalId"), Permission.Effect.valueOf(fields.text("effect")),
|
fields.text("principalId"), Permission.Effect.valueOf(fields.text("effect")),
|
||||||
Permission.Action.valueOf(fields.text("action")),
|
Permission.Action.valueOf(fields.text("action")),
|
||||||
@@ -432,11 +435,18 @@ final class HttpOperationCodec {
|
|||||||
fields.exact("accountId"); yield new ServerControlOperation.DeactivateAcmeAccount(fields.text("accountId"));
|
fields.exact("accountId"); yield new ServerControlOperation.DeactivateAcmeAccount(fields.text("accountId"));
|
||||||
}
|
}
|
||||||
case ServerControlOperation.RegisterOcspResponder.NAME -> {
|
case ServerControlOperation.RegisterOcspResponder.NAME -> {
|
||||||
fields.exact("responderId", "alias", "authorityId", "issuerId", "signingMode",
|
fields.allowed(Set.of("responderId", "alias", "authorityId", "issuerId", "signingMode",
|
||||||
"responderCredentialId", "signingKeyRef", "chainPathId", "signatureAlgorithm",
|
"responderCredentialId", "signingKeyRef", "chainPathId", "signatureAlgorithm",
|
||||||
"signatureBindingId", "signatureBindingCommitment", "responderIdForm",
|
"signatureBindingId", "signatureBindingCommitment", "responderIdForm",
|
||||||
"responseValidityMillis", "noncePolicy", "maximumNonceBytes", "acceptedHashes",
|
"responseValidityMillis", "noncePolicy", "maximumNonceBytes", "acceptedHashes",
|
||||||
"maximumRequestBytes", "maximumEntries", "cacheLifetimeMillis");
|
"maximumRequestBytes", "maximumEntries", "cacheLifetimeMillis",
|
||||||
|
"requestAuthenticationMode", "requesterTrustPolicy"));
|
||||||
|
OcspResponderService.RequestAuthenticationMode authenticationMode =
|
||||||
|
OcspResponderService.RequestAuthenticationMode.valueOf(
|
||||||
|
fields.text("requestAuthenticationMode"));
|
||||||
|
Optional<OcspResponderService.RequesterTrustPolicy> trustPolicy = fields.contains(
|
||||||
|
"requesterTrustPolicy") ? Optional.of(requesterTrustPolicy(
|
||||||
|
fields.object("requesterTrustPolicy"))) : Optional.empty();
|
||||||
yield new ServerControlOperation.RegisterOcspResponder(new OcspResponderService.Registration(
|
yield new ServerControlOperation.RegisterOcspResponder(new OcspResponderService.Registration(
|
||||||
fields.text("responderId"), fields.text("alias"),
|
fields.text("responderId"), fields.text("alias"),
|
||||||
fields.pkiId("authorityId"), fields.pkiId("issuerId"),
|
fields.pkiId("authorityId"), fields.pkiId("issuerId"),
|
||||||
@@ -450,7 +460,8 @@ final class HttpOperationCodec {
|
|||||||
fields.integer("maximumNonceBytes"),
|
fields.integer("maximumNonceBytes"),
|
||||||
fields.enumSet("acceptedHashes", OcspResponseService.CertIdHash.class),
|
fields.enumSet("acceptedHashes", OcspResponseService.CertIdHash.class),
|
||||||
fields.integer("maximumRequestBytes"), fields.integer("maximumEntries"),
|
fields.integer("maximumRequestBytes"), fields.integer("maximumEntries"),
|
||||||
Duration.ofMillis(fields.longValue("cacheLifetimeMillis"))));
|
Duration.ofMillis(fields.longValue("cacheLifetimeMillis")), authenticationMode,
|
||||||
|
trustPolicy));
|
||||||
}
|
}
|
||||||
case ServerControlOperation.InspectOcspResponder.NAME -> {
|
case ServerControlOperation.InspectOcspResponder.NAME -> {
|
||||||
fields.exact("responderId"); yield new ServerControlOperation.InspectOcspResponder(fields.text("responderId"));
|
fields.exact("responderId"); yield new ServerControlOperation.InspectOcspResponder(fields.text("responderId"));
|
||||||
@@ -553,6 +564,34 @@ final class HttpOperationCodec {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static OcspResponderService.RequesterTrustPolicy requesterTrustPolicy(Fields fields) {
|
||||||
|
fields.exact("trustSelectors", "principalMappings", "signatureBindings", "revocationPolicy",
|
||||||
|
"maximumSignerCertificates", "maximumSignerChainBytes");
|
||||||
|
List<OcspResponderService.TrustSelector> selectors = fields.objects("trustSelectors").stream()
|
||||||
|
.map(value -> {
|
||||||
|
value.exact("type", "identities");
|
||||||
|
return new OcspResponderService.TrustSelector(
|
||||||
|
OcspResponderService.TrustSelectorType.valueOf(value.text("type")),
|
||||||
|
value.stringSet("identities"));
|
||||||
|
}).toList();
|
||||||
|
List<OcspResponderService.PrincipalMapping> mappings = fields.objects("principalMappings").stream()
|
||||||
|
.map(value -> {
|
||||||
|
value.exact("mappingId", "principalId", "type", "commitment");
|
||||||
|
return new OcspResponderService.PrincipalMapping(value.text("mappingId"),
|
||||||
|
value.text("principalId"), OcspResponderService.PrincipalMappingType.valueOf(
|
||||||
|
value.text("type")), value.text("commitment"));
|
||||||
|
}).toList();
|
||||||
|
List<OcspResponderService.RequestSignatureBinding> bindings = fields.objects("signatureBindings").stream()
|
||||||
|
.map(value -> {
|
||||||
|
value.exact("bindingId", "commitment");
|
||||||
|
return new OcspResponderService.RequestSignatureBinding(value.text("bindingId"),
|
||||||
|
value.text("commitment"));
|
||||||
|
}).toList();
|
||||||
|
return new OcspResponderService.RequesterTrustPolicy(selectors, mappings, bindings,
|
||||||
|
OcspResponderService.RequesterRevocationPolicy.valueOf(fields.text("revocationPolicy")),
|
||||||
|
fields.integer("maximumSignerCertificates"), fields.integer("maximumSignerChainBytes"));
|
||||||
|
}
|
||||||
|
|
||||||
private static Duration duration(String value) {
|
private static Duration duration(String value) {
|
||||||
try {
|
try {
|
||||||
Duration result = Duration.parse(value);
|
Duration result = Duration.parse(value);
|
||||||
@@ -587,6 +626,7 @@ final class HttpOperationCodec {
|
|||||||
if (!(value instanceof PkiOperationValue.Text text)) throw type(); return text.value(); }
|
if (!(value instanceof PkiOperationValue.Text text)) throw type(); return text.value(); }
|
||||||
Optional<String> optionalText(String name) { return fields.containsKey(name)
|
Optional<String> optionalText(String name) { return fields.containsKey(name)
|
||||||
? Optional.of(text(name)) : Optional.empty(); }
|
? Optional.of(text(name)) : Optional.empty(); }
|
||||||
|
boolean contains(String name) { return fields.containsKey(name); }
|
||||||
long longValue(String name) { consumed.add(name); PkiOperationValue value = require(name);
|
long longValue(String name) { consumed.add(name); PkiOperationValue value = require(name);
|
||||||
if (!(value instanceof PkiOperationValue.IntegerValue integer)) throw type(); return integer.value(); }
|
if (!(value instanceof PkiOperationValue.IntegerValue integer)) throw type(); return integer.value(); }
|
||||||
int integer(String name) { return Math.toIntExact(longValue(name)); }
|
int integer(String name) { return Math.toIntExact(longValue(name)); }
|
||||||
@@ -618,6 +658,11 @@ final class HttpOperationCodec {
|
|||||||
return Set.copyOf(result);
|
return Set.copyOf(result);
|
||||||
}
|
}
|
||||||
Fields object(String name) { consumed.add(name); return of(require(name)); }
|
Fields object(String name) { consumed.add(name); return of(require(name)); }
|
||||||
|
List<Fields> objects(String name) {
|
||||||
|
consumed.add(name); PkiOperationValue value = require(name);
|
||||||
|
if (!(value instanceof PkiOperationValue.ListValue list)) throw type();
|
||||||
|
return list.values().stream().map(Fields::of).toList();
|
||||||
|
}
|
||||||
void complete() { if (!Objects.equals(consumed, fields.keySet()))
|
void complete() { if (!Objects.equals(consumed, fields.keySet()))
|
||||||
throw new IllegalArgumentException("Request fields were not consumed"); }
|
throw new IllegalArgumentException("Request fields were not consumed"); }
|
||||||
private PkiOperationValue require(String name) { PkiOperationValue value = fields.get(name);
|
private PkiOperationValue require(String name) { PkiOperationValue value = fields.get(name);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import zeroecho.pki.application.OcspResponseService;
|
|||||||
import zeroecho.pki.application.PkiRepositoryContent;
|
import zeroecho.pki.application.PkiRepositoryContent;
|
||||||
import zeroecho.pki.server.AdministrativeAuthenticationMode;
|
import zeroecho.pki.server.AdministrativeAuthenticationMode;
|
||||||
import zeroecho.pki.server.OcspResponderService;
|
import zeroecho.pki.server.OcspResponderService;
|
||||||
|
import zeroecho.pki.server.OcspRequesterService;
|
||||||
import zeroecho.pki.server.PkiServerConfiguration;
|
import zeroecho.pki.server.PkiServerConfiguration;
|
||||||
import zeroecho.pki.server.ServerRealmContext;
|
import zeroecho.pki.server.ServerRealmContext;
|
||||||
import zeroecho.pki.server.spi.PkiServerAuthenticationContext;
|
import zeroecho.pki.server.spi.PkiServerAuthenticationContext;
|
||||||
@@ -86,6 +87,8 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final RequestIds requestIds;
|
private final RequestIds requestIds;
|
||||||
private final BooleanSupplier ready;
|
private final BooleanSupplier ready;
|
||||||
|
private final OcspRequesterService requesters;
|
||||||
|
private final NonceReplayGuard nonceReplay = new NonceReplayGuard();
|
||||||
|
|
||||||
OcspHttpHandler(PkiServerConfiguration.PublicListener configuration, ServerRealmContext realm,
|
OcspHttpHandler(PkiServerConfiguration.PublicListener configuration, ServerRealmContext realm,
|
||||||
AdministrativeAuthenticator authenticator, ServerRuntime runtime, Clock clock,
|
AdministrativeAuthenticator authenticator, ServerRuntime runtime, Clock clock,
|
||||||
@@ -94,40 +97,63 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
this.authenticator = java.util.Objects.requireNonNull(authenticator); this.runtime = java.util.Objects.requireNonNull(runtime);
|
this.authenticator = java.util.Objects.requireNonNull(authenticator); this.runtime = java.util.Objects.requireNonNull(runtime);
|
||||||
this.clock = java.util.Objects.requireNonNull(clock); this.requestIds = java.util.Objects.requireNonNull(requestIds);
|
this.clock = java.util.Objects.requireNonNull(clock); this.requestIds = java.util.Objects.requireNonNull(requestIds);
|
||||||
this.ready = java.util.Objects.requireNonNull(ready);
|
this.ready = java.util.Objects.requireNonNull(ready);
|
||||||
|
this.requesters = new OcspRequesterService(realm, clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void handle(HttpExchange exchange) throws IOException {
|
@Override public void handle(HttpExchange exchange) throws IOException {
|
||||||
String requestId = "unavailable-request"; boolean admitted = false;
|
String requestId = "unavailable-request"; boolean admitted = false;
|
||||||
OcspResponderService.Responder auditedResponder = null;
|
OcspResponderService.Responder auditedResponder = null;
|
||||||
|
Optional<String> transportPrincipal = Optional.empty();
|
||||||
|
Optional<String> requesterPrincipal = Optional.empty();
|
||||||
try {
|
try {
|
||||||
requestId = requestIds.resolve(exchange.getRequestHeaders().get(RequestIds.HEADER));
|
requestId = requestIds.resolve(exchange.getRequestHeaders().get(RequestIds.HEADER));
|
||||||
if (!ready.getAsBoolean()) { transportFailure(exchange, 503); return; }
|
if (!ready.getAsBoolean()) { transportFailure(exchange, 503); return; }
|
||||||
requireHeadersBounded(exchange.getRequestHeaders());
|
requireHeadersBounded(exchange.getRequestHeaders());
|
||||||
validateTransport(exchange, requestId);
|
transportPrincipal = validateTransport(exchange, requestId);
|
||||||
Route route = route(exchange);
|
Route route = route(exchange);
|
||||||
|
if (!runtime.tryAdmit()) { audit(requestId, Optional.empty(), transportPrincipal, Optional.empty(),
|
||||||
|
"OVERLOAD"); transportFailure(exchange, 429); return; }
|
||||||
|
admitted = true;
|
||||||
OcspResponderService.Responder responder = activeResponder(route.alias());
|
OcspResponderService.Responder responder = activeResponder(route.alias());
|
||||||
auditedResponder = responder;
|
auditedResponder = responder;
|
||||||
audit(requestId, Optional.of(responder), "ACCEPTED");
|
audit(requestId, Optional.of(responder), transportPrincipal, Optional.empty(), "ACCEPTED");
|
||||||
if (!runtime.tryAdmit()) { audit(requestId, Optional.of(responder), "OVERLOAD"); transportFailure(exchange, 429); return; }
|
|
||||||
admitted = true;
|
|
||||||
int maximumRequestBytes = Math.min(configuration.maximumBodyBytes(), responder.maximumRequestBytes());
|
int maximumRequestBytes = Math.min(configuration.maximumBodyBytes(), responder.maximumRequestBytes());
|
||||||
byte[] request = route.encoded().orElseGet(() -> read(exchange, maximumRequestBytes));
|
byte[] request = route.encoded().orElseGet(() -> read(exchange, maximumRequestBytes));
|
||||||
if (request.length > maximumRequestBytes) { transportFailure(exchange, 413); return; }
|
if (request.length > maximumRequestBytes) { transportFailure(exchange, 413); return; }
|
||||||
OcspRequestParser.Parsed parsed = OcspRequestParser.parse(request, responder.maximumEntries(),
|
OcspRequestParser.Parsed parsed = OcspRequestParser.parse(request, responder.maximumEntries(),
|
||||||
responder.acceptedHashes(), responder.maximumNonceBytes());
|
responder.acceptedHashes(), responder.maximumNonceBytes(),
|
||||||
|
responder.requesterTrustPolicy().map(OcspResponderService.RequesterTrustPolicy
|
||||||
|
::maximumSignerCertificates).orElse(16),
|
||||||
|
responder.requesterTrustPolicy().map(OcspResponderService.RequesterTrustPolicy
|
||||||
|
::maximumSignerChainBytes).orElse(maximumRequestBytes));
|
||||||
validateNonce(responder.noncePolicy(), parsed.nonce());
|
validateNonce(responder.noncePolicy(), parsed.nonce());
|
||||||
OcspResponseService.Response response = execute(responder, parsed);
|
requireAuthenticationMode(responder.requestAuthenticationMode(), parsed.signedRequest());
|
||||||
send(exchange, responder, response, parsed.nonce().isPresent(), requestId);
|
Execution execution = execute(responder, parsed);
|
||||||
audit(requestId, Optional.of(responder), "RESPONDED_GOOD_" + response.goodCount()
|
requesterPrincipal = execution.requesterPrincipal();
|
||||||
|
send(exchange, responder, execution.response(), parsed.nonce().isPresent(),
|
||||||
|
parsed.signedRequest().isPresent(), requestId);
|
||||||
|
OcspResponseService.Response response = execution.response();
|
||||||
|
audit(requestId, Optional.of(responder), transportPrincipal, requesterPrincipal,
|
||||||
|
"RESPONDED_GOOD_" + response.goodCount()
|
||||||
+ "_REVOKED_" + response.revokedCount() + "_UNKNOWN_" + response.unknownCount());
|
+ "_REVOKED_" + response.revokedCount() + "_UNKNOWN_" + response.unknownCount());
|
||||||
} catch (UnknownAlias unavailable) { audit(requestId, Optional.empty(), "UNKNOWN_RESPONDER"); transportFailure(exchange, 404);
|
} catch (SignatureRequired required) { audit(requestId, Optional.ofNullable(auditedResponder),
|
||||||
} catch (InactiveResponder inactive) { audit(requestId, Optional.empty(), "INACTIVE_RESPONDER"); transportFailure(exchange, 503);
|
transportPrincipal, requesterPrincipal, "SIGNATURE_REQUIRED"); protocolFailure(exchange,
|
||||||
} catch (MethodFailure method) { audit(requestId, Optional.ofNullable(auditedResponder), "METHOD_REJECTED"); transportFailure(exchange, 405);
|
org.bouncycastle.cert.ocsp.OCSPRespBuilder.SIG_REQUIRED);
|
||||||
} catch (MediaFailure media) { audit(requestId, Optional.ofNullable(auditedResponder), "MEDIA_REJECTED"); transportFailure(exchange, 406);
|
} catch (OcspRequesterService.UnauthorizedException unauthorized) { audit(requestId,
|
||||||
} catch (IllegalArgumentException malformed) { audit(requestId, Optional.ofNullable(auditedResponder), "MALFORMED"); protocolFailure(exchange, org.bouncycastle.cert.ocsp.OCSPRespBuilder.MALFORMED_REQUEST);
|
Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal,
|
||||||
} catch (RejectedExecutionException overload) { audit(requestId, Optional.ofNullable(auditedResponder), "OVERLOAD"); transportFailure(exchange, 429);
|
"REQUESTER_UNAUTHORIZED_" + unauthorized.classification()); protocolFailure(exchange,
|
||||||
} catch (TimeoutException deadline) { audit(requestId, Optional.ofNullable(auditedResponder), "DEADLINE"); transportFailure(exchange, 504);
|
org.bouncycastle.cert.ocsp.OCSPRespBuilder.UNAUTHORIZED);
|
||||||
} catch (RuntimeException failure) { audit(requestId, Optional.ofNullable(auditedResponder), "UNAVAILABLE"); transportFailure(exchange, 503);
|
} catch (OcspRequesterService.UnavailableException unavailable) { audit(requestId,
|
||||||
|
Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal,
|
||||||
|
"REQUESTER_AUTHORITY_UNAVAILABLE"); protocolFailure(exchange, org.bouncycastle.cert.ocsp.OCSPRespBuilder.TRY_LATER);
|
||||||
|
} catch (UnknownAlias unavailable) { audit(requestId, Optional.empty(), transportPrincipal, requesterPrincipal, "UNKNOWN_RESPONDER"); transportFailure(exchange, 404);
|
||||||
|
} catch (InactiveResponder inactive) { audit(requestId, Optional.empty(), transportPrincipal, requesterPrincipal, "INACTIVE_RESPONDER"); transportFailure(exchange, 503);
|
||||||
|
} catch (MethodFailure method) { audit(requestId, Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal, "METHOD_REJECTED"); transportFailure(exchange, 405);
|
||||||
|
} catch (MediaFailure media) { audit(requestId, Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal, "MEDIA_REJECTED"); transportFailure(exchange, 406);
|
||||||
|
} catch (IllegalArgumentException malformed) { audit(requestId, Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal, "MALFORMED"); protocolFailure(exchange, org.bouncycastle.cert.ocsp.OCSPRespBuilder.MALFORMED_REQUEST);
|
||||||
|
} catch (RejectedExecutionException overload) { audit(requestId, Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal, "OVERLOAD"); transportFailure(exchange, 429);
|
||||||
|
} catch (TimeoutException deadline) { audit(requestId, Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal, "DEADLINE"); transportFailure(exchange, 504);
|
||||||
|
} catch (RuntimeException failure) { audit(requestId, Optional.ofNullable(auditedResponder), transportPrincipal, requesterPrincipal, "UNAVAILABLE"); transportFailure(exchange, 503);
|
||||||
} finally { if (admitted) runtime.releaseAdmission(); exchange.close(); }
|
} finally { if (admitted) runtime.releaseAdmission(); exchange.close(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +167,7 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private OcspResponseService.Response execute(OcspResponderService.Responder responder,
|
private Execution execute(OcspResponderService.Responder responder,
|
||||||
OcspRequestParser.Parsed parsed) throws TimeoutException {
|
OcspRequestParser.Parsed parsed) throws TimeoutException {
|
||||||
IssuerGeneration issuer = realm.session().repository().issuer(responder.issuerId()).orElseThrow();
|
IssuerGeneration issuer = realm.session().repository().issuer(responder.issuerId()).orElseThrow();
|
||||||
IssuerChainPath path = realm.session().repository().chainPath(responder.chainPathId()).orElseThrow();
|
IssuerChainPath path = realm.session().repository().chainPath(responder.chainPathId()).orElseThrow();
|
||||||
@@ -156,15 +182,26 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
responder.signingKeyRef(), chain, responder.signatureAlgorithm(), responder.signatureBindingId(),
|
responder.signingKeyRef(), chain, responder.signatureAlgorithm(), responder.signatureBindingId(),
|
||||||
responder.responderIdForm(), produced, produced, produced.plus(responder.responseValidity()),
|
responder.responderIdForm(), produced, produced, produced.plus(responder.responseValidity()),
|
||||||
parsed.nonce(), parsed.requests());
|
parsed.nonce(), parsed.requests());
|
||||||
ServerRuntime.Submitted<OcspResponseService.Response> submitted = runtime.submit(cancellation -> {
|
ServerRuntime.Submitted<Execution> submitted = runtime.submit(cancellation -> {
|
||||||
cancellation.throwIfCancelled();
|
cancellation.throwIfCancelled();
|
||||||
return realm.session().ocsp().orElseThrow().respond(command);
|
Optional<String> requester = parsed.signedRequest().map(value -> requesters.authenticate(responder,
|
||||||
|
value, parsed.requests()).principalId());
|
||||||
|
cancellation.throwIfCancelled();
|
||||||
|
if (responder.noncePolicy() == OcspResponderService.NoncePolicy.REQUIRED
|
||||||
|
&& !nonceReplay.accept(responder.responderId(), parsed.nonce().orElseThrow(), clock.instant(),
|
||||||
|
responder.responseValidity())) {
|
||||||
|
throw new IllegalArgumentException("OCSP nonce was already used");
|
||||||
|
}
|
||||||
|
return new Execution(realm.session().ocsp().orElseThrow().respond(command), requester);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
return submitted.future().get(configuration.maximumStreamDuration().toMillis(), TimeUnit.MILLISECONDS);
|
return submitted.future().get(configuration.maximumStreamDuration().toMillis(), TimeUnit.MILLISECONDS);
|
||||||
} catch (InterruptedException failure) {
|
} catch (InterruptedException failure) {
|
||||||
submitted.cancellation().cancel(); submitted.future().cancel(true); Thread.currentThread().interrupt();
|
submitted.cancellation().cancel(); submitted.future().cancel(true); Thread.currentThread().interrupt();
|
||||||
throw new TimeoutException("OCSP operation interrupted");
|
throw new TimeoutException("OCSP operation interrupted");
|
||||||
|
} catch (TimeoutException failure) {
|
||||||
|
submitted.cancellation().cancel(); submitted.future().cancel(true);
|
||||||
|
throw failure;
|
||||||
} catch (ExecutionException failure) {
|
} catch (ExecutionException failure) {
|
||||||
if (failure.getCause() instanceof RuntimeException runtimeFailure) throw runtimeFailure;
|
if (failure.getCause() instanceof RuntimeException runtimeFailure) throw runtimeFailure;
|
||||||
throw new IllegalStateException("OCSP operation failed");
|
throw new IllegalStateException("OCSP operation failed");
|
||||||
@@ -193,17 +230,19 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateTransport(HttpExchange exchange, String requestId) {
|
private Optional<String> validateTransport(HttpExchange exchange, String requestId) {
|
||||||
Map<String, List<String>> headers = Map.copyOf(exchange.getRequestHeaders());
|
Map<String, List<String>> headers = Map.copyOf(exchange.getRequestHeaders());
|
||||||
boolean forwarded = ForwardedClientCertificateParser.containsForwardedIdentity(headers);
|
boolean forwarded = ForwardedClientCertificateParser.containsForwardedIdentity(headers);
|
||||||
if (configuration.authentication().mode() == AdministrativeAuthenticationMode.DIRECT_MTLS) {
|
if (configuration.authentication().mode() == AdministrativeAuthenticationMode.DIRECT_MTLS) {
|
||||||
if (forwarded) throw new IllegalArgumentException("Forwarded identity is prohibited");
|
if (forwarded) throw new IllegalArgumentException("Forwarded identity is prohibited");
|
||||||
return;
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
Optional<PkiServerAuthenticationContext> context = tlsContext(exchange, requestId, headers);
|
Optional<PkiServerAuthenticationContext> context = tlsContext(exchange, requestId, headers);
|
||||||
if (context.isEmpty() || authenticator.authenticatePublicProxyTransport(context.orElseThrow()).isEmpty()) {
|
Optional<String> transport = context.flatMap(authenticator::authenticatePublicProxyTransport);
|
||||||
|
if (transport.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Proxy transport is unauthenticated");
|
throw new IllegalArgumentException("Proxy transport is unauthenticated");
|
||||||
}
|
}
|
||||||
|
return transport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<PkiServerAuthenticationContext> tlsContext(HttpExchange exchange, String requestId,
|
private Optional<PkiServerAuthenticationContext> tlsContext(HttpExchange exchange, String requestId,
|
||||||
@@ -282,10 +321,11 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void send(HttpExchange exchange, OcspResponderService.Responder responder,
|
private void send(HttpExchange exchange, OcspResponderService.Responder responder,
|
||||||
OcspResponseService.Response response, boolean nonce, String requestId) throws IOException {
|
OcspResponseService.Response response, boolean nonce, boolean signed, String requestId) throws IOException {
|
||||||
Headers headers = exchange.getResponseHeaders(); headers.set("Content-Type", MEDIA_RESPONSE);
|
Headers headers = exchange.getResponseHeaders(); headers.set("Content-Type", MEDIA_RESPONSE);
|
||||||
headers.set("X-Content-Type-Options", "nosniff"); headers.set(RequestIds.HEADER, requestId);
|
headers.set("X-Content-Type-Options", "nosniff"); headers.set(RequestIds.HEADER, requestId);
|
||||||
if (nonce) { headers.set("Cache-Control", "no-store"); }
|
if (signed) { headers.set("Cache-Control", "private, no-store"); headers.set("Pragma", "no-cache"); }
|
||||||
|
else if (nonce) { headers.set("Cache-Control", "no-store"); }
|
||||||
else {
|
else {
|
||||||
long seconds = Math.min(responder.cacheLifetime().toSeconds(), responder.responseValidity().toSeconds());
|
long seconds = Math.min(responder.cacheLifetime().toSeconds(), responder.responseValidity().toSeconds());
|
||||||
String validator = etag(response.der());
|
String validator = etag(response.der());
|
||||||
@@ -315,19 +355,46 @@ final class OcspHttpHandler implements HttpHandler {
|
|||||||
exchange.sendResponseHeaders(status, -1);
|
exchange.sendResponseHeaders(status, -1);
|
||||||
}
|
}
|
||||||
private void audit(String requestId, Optional<OcspResponderService.Responder> responder,
|
private void audit(String requestId, Optional<OcspResponderService.Responder> responder,
|
||||||
String classification) {
|
Optional<String> transportPrincipal, Optional<String> requesterPrincipal, String classification) {
|
||||||
Map<String, String> details = new java.util.LinkedHashMap<>();
|
Map<String, String> details = new java.util.LinkedHashMap<>();
|
||||||
details.put("request", requestId); details.put("classification", classification);
|
details.put("request", requestId); details.put("classification", classification);
|
||||||
responder.ifPresent(value -> details.put("responder", value.responderId()));
|
responder.ifPresent(value -> details.put("responder", value.responderId()));
|
||||||
realm.auditTransport("OCSP_REQUEST", "anonymous", Map.copyOf(details));
|
transportPrincipal.ifPresent(value -> details.put("transportPrincipal", value));
|
||||||
|
requesterPrincipal.ifPresent(value -> details.put("requesterPrincipal", value));
|
||||||
|
realm.auditTransport("OCSP_REQUEST", requesterPrincipal.orElse("anonymous"), Map.copyOf(details));
|
||||||
}
|
}
|
||||||
private static String etag(byte[] value) {
|
private static String etag(byte[] value) {
|
||||||
try { return '"' + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value)) + '"'; }
|
try { return '"' + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value)) + '"'; }
|
||||||
catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException("SHA-256 unavailable", impossible); }
|
catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException("SHA-256 unavailable", impossible); }
|
||||||
}
|
}
|
||||||
private record Route(String alias, Optional<byte[]> encoded) { Route { encoded = encoded.map(byte[]::clone); } }
|
private record Route(String alias, Optional<byte[]> encoded) { Route { encoded = encoded.map(byte[]::clone); } }
|
||||||
|
private record Execution(OcspResponseService.Response response, Optional<String> requesterPrincipal) {
|
||||||
|
Execution { java.util.Objects.requireNonNull(response); requesterPrincipal = java.util.Objects.requireNonNull(requesterPrincipal); }
|
||||||
|
}
|
||||||
|
private static void requireAuthenticationMode(OcspResponderService.RequestAuthenticationMode mode,
|
||||||
|
Optional<OcspRequesterService.SignedRequest> signed) {
|
||||||
|
if (mode == OcspResponderService.RequestAuthenticationMode.AUTHORIZED_SIGNED_REQUIRED && signed.isEmpty()) {
|
||||||
|
throw new SignatureRequired();
|
||||||
|
}
|
||||||
|
if (mode == OcspResponderService.RequestAuthenticationMode.UNSIGNED_ONLY && signed.isPresent()) {
|
||||||
|
throw new OcspRequesterService.UnauthorizedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
private static final class UnknownAlias extends RuntimeException { private static final long serialVersionUID = 1L; }
|
private static final class UnknownAlias extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
private static final class InactiveResponder extends RuntimeException { private static final long serialVersionUID = 1L; }
|
private static final class InactiveResponder extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
private static final class MethodFailure extends RuntimeException { private static final long serialVersionUID = 1L; }
|
private static final class MethodFailure extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
private static final class MediaFailure extends RuntimeException { private static final long serialVersionUID = 1L; }
|
private static final class MediaFailure extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
|
private static final class SignatureRequired extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
|
private static final class NonceReplayGuard {
|
||||||
|
private static final int MAXIMUM_ENTRIES = 4096;
|
||||||
|
private final java.util.LinkedHashMap<String, Instant> accepted = new java.util.LinkedHashMap<>();
|
||||||
|
synchronized boolean accept(String responderId, byte[] nonce, Instant now, java.time.Duration lifetime) {
|
||||||
|
accepted.entrySet().removeIf(entry -> !now.isBefore(entry.getValue()));
|
||||||
|
String key = etag((responderId + ':' + HexFormat.of().formatHex(nonce))
|
||||||
|
.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
if (accepted.containsKey(key) || accepted.size() >= MAXIMUM_ENTRIES) return false;
|
||||||
|
accepted.put(key, now.plus(lifetime));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,26 +38,37 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1Primitive;
|
||||||
import org.bouncycastle.asn1.ASN1OctetString;
|
import org.bouncycastle.asn1.ASN1OctetString;
|
||||||
import org.bouncycastle.asn1.DERNull;
|
import org.bouncycastle.asn1.DERNull;
|
||||||
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
|
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
|
||||||
|
import org.bouncycastle.asn1.ocsp.OCSPRequest;
|
||||||
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
|
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
|
||||||
|
import org.bouncycastle.asn1.ocsp.Signature;
|
||||||
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
|
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralName;
|
||||||
import org.bouncycastle.cert.ocsp.CertificateID;
|
import org.bouncycastle.cert.ocsp.CertificateID;
|
||||||
import org.bouncycastle.cert.ocsp.OCSPReq;
|
import org.bouncycastle.cert.ocsp.OCSPReq;
|
||||||
import org.bouncycastle.cert.ocsp.Req;
|
import org.bouncycastle.cert.ocsp.Req;
|
||||||
|
|
||||||
import zeroecho.pki.application.OcspResponseService;
|
import zeroecho.pki.application.OcspResponseService;
|
||||||
|
import zeroecho.pki.server.OcspRequesterService;
|
||||||
|
|
||||||
/** Narrow canonical DER and unpadded Base64url OCSP request decoder. */
|
/** Narrow canonical DER and unpadded Base64url OCSP request decoder. */
|
||||||
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.ExceptionAsFlowControl", "PMD.PreserveStackTrace",
|
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.ExceptionAsFlowControl", "PMD.PreserveStackTrace",
|
||||||
"PMD.AvoidCatchingGenericException", "PMD.CyclomaticComplexity" })
|
"PMD.AvoidCatchingGenericException", "PMD.CyclomaticComplexity", "PMD.AvoidLiteralsInIfCondition" })
|
||||||
final class OcspRequestParser {
|
final class OcspRequestParser {
|
||||||
/* default */ record Parsed(List<OcspResponseService.CertId> requests, Optional<byte[]> nonce) {
|
/* default */ record Parsed(List<OcspResponseService.CertId> requests, Optional<byte[]> nonce,
|
||||||
Parsed { requests = List.copyOf(requests); nonce = nonce.map(byte[]::clone); }
|
Optional<OcspRequesterService.SignedRequest> signedRequest) {
|
||||||
|
Parsed {
|
||||||
|
requests = List.copyOf(requests);
|
||||||
|
nonce = nonce.map(byte[]::clone);
|
||||||
|
signedRequest = Objects.requireNonNull(signedRequest, "signedRequest");
|
||||||
|
}
|
||||||
@Override public Optional<byte[]> nonce() { return nonce.map(byte[]::clone); }
|
@Override public Optional<byte[]> nonce() { return nonce.map(byte[]::clone); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +87,21 @@ final class OcspRequestParser {
|
|||||||
|
|
||||||
/* default */ static Parsed parse(byte[] der, int maximumEntries,
|
/* default */ static Parsed parse(byte[] der, int maximumEntries,
|
||||||
Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumNonceBytes) {
|
Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumNonceBytes) {
|
||||||
|
return parse(der, maximumEntries, acceptedHashes, maximumNonceBytes, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default */ static Parsed parse(byte[] der, int maximumEntries,
|
||||||
|
Set<OcspResponseService.CertIdHash> acceptedHashes, int maximumNonceBytes,
|
||||||
|
int maximumSignerCertificates, int maximumSignerChainBytes) {
|
||||||
try {
|
try {
|
||||||
OCSPReq request = new OCSPReq(der);
|
OCSPReq request = new OCSPReq(der);
|
||||||
if (!Arrays.equals(der, request.getEncoded()) || request.isSigned()) throw malformed();
|
if (!Arrays.equals(der, request.getEncoded())) throw malformed();
|
||||||
|
byte[] exactTbsRequest = firstSequenceElement(der);
|
||||||
Req[] entries = request.getRequestList();
|
Req[] entries = request.getRequestList();
|
||||||
if (entries.length == 0 || entries.length > maximumEntries) throw malformed();
|
if (entries.length == 0 || entries.length > maximumEntries) throw malformed();
|
||||||
List<OcspResponseService.CertId> result = new ArrayList<>(entries.length);
|
List<OcspResponseService.CertId> result = new ArrayList<>(entries.length);
|
||||||
for (Req entry : entries) {
|
for (Req entry : entries) {
|
||||||
|
if (entry.getSingleRequestExtensions() != null) throw malformed();
|
||||||
CertificateID id = entry.getCertID();
|
CertificateID id = entry.getCertID();
|
||||||
org.bouncycastle.asn1.x509.AlgorithmIdentifier algorithm = id.toASN1Primitive().getHashAlgorithm();
|
org.bouncycastle.asn1.x509.AlgorithmIdentifier algorithm = id.toASN1Primitive().getHashAlgorithm();
|
||||||
OcspResponseService.CertIdHash hash;
|
OcspResponseService.CertIdHash hash;
|
||||||
@@ -110,10 +129,73 @@ final class OcspRequestParser {
|
|||||||
if (value.length == 0 || value.length > maximumNonceBytes) throw malformed();
|
if (value.length == 0 || value.length > maximumNonceBytes) throw malformed();
|
||||||
nonce = Optional.of(value);
|
nonce = Optional.of(value);
|
||||||
}
|
}
|
||||||
return new Parsed(result, nonce);
|
Optional<OcspRequesterService.SignedRequest> signed = signedRequest(request, der, exactTbsRequest,
|
||||||
|
maximumSignerCertificates, maximumSignerChainBytes);
|
||||||
|
if (signed.isEmpty() && request.getRequestorName() != null) throw malformed();
|
||||||
|
return new Parsed(result, nonce, signed);
|
||||||
} catch (IOException | RuntimeException failure) { throw malformed(); }
|
} catch (IOException | RuntimeException failure) { throw malformed(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Optional<OcspRequesterService.SignedRequest> signedRequest(OCSPReq request, byte[] requestDer,
|
||||||
|
byte[] exactTbsRequest, int maximumSignerCertificates, int maximumSignerChainBytes) throws IOException {
|
||||||
|
if (!request.isSigned()) return Optional.empty();
|
||||||
|
if (maximumSignerCertificates <= 0 || maximumSignerChainBytes <= 0) throw malformed();
|
||||||
|
GeneralName requestor = request.getRequestorName();
|
||||||
|
if (requestor == null || requestor.getTagNo() != GeneralName.directoryName) throw malformed();
|
||||||
|
byte[] nameDer = requestor.getName().toASN1Primitive().getEncoded("DER");
|
||||||
|
OCSPRequest structure = OCSPRequest.getInstance(ASN1Primitive.fromByteArray(requestDer));
|
||||||
|
Signature signature = structure.getOptionalSignature();
|
||||||
|
if (signature == null || signature.getSignature().getPadBits() != 0
|
||||||
|
|| signature.getSignature().getOctets().length == 0) throw malformed();
|
||||||
|
byte[] algorithmDer = signature.getSignatureAlgorithm().getEncoded("DER");
|
||||||
|
org.bouncycastle.cert.X509CertificateHolder[] holders = request.getCerts();
|
||||||
|
if (holders.length == 0 || holders.length > maximumSignerCertificates) throw malformed();
|
||||||
|
int total = 0;
|
||||||
|
List<byte[]> certificates = new ArrayList<>(holders.length);
|
||||||
|
Set<String> unique = new java.util.HashSet<>();
|
||||||
|
for (org.bouncycastle.cert.X509CertificateHolder holder : holders) {
|
||||||
|
byte[] certificate = holder.getEncoded();
|
||||||
|
if (!Arrays.equals(certificate, ASN1Primitive.fromByteArray(certificate).getEncoded("DER"))) {
|
||||||
|
throw malformed();
|
||||||
|
}
|
||||||
|
total = Math.addExact(total, certificate.length);
|
||||||
|
if (total > maximumSignerChainBytes || !unique.add(Base64.getEncoder().encodeToString(certificate))) {
|
||||||
|
throw malformed();
|
||||||
|
}
|
||||||
|
certificates.add(certificate);
|
||||||
|
}
|
||||||
|
return Optional.of(new OcspRequesterService.SignedRequest(exactTbsRequest, nameDer, algorithmDer,
|
||||||
|
signature.getSignature().getOctets(), certificates));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] firstSequenceElement(byte[] der) {
|
||||||
|
if (der.length < 4 || Byte.toUnsignedInt(der[0]) != 0x30) throw malformed();
|
||||||
|
Length outer = length(der, 1);
|
||||||
|
int outerEnd = Math.addExact(outer.offset(), outer.length());
|
||||||
|
if (outerEnd != der.length || outer.offset() >= outerEnd || Byte.toUnsignedInt(der[outer.offset()]) != 0x30) {
|
||||||
|
throw malformed();
|
||||||
|
}
|
||||||
|
Length child = length(der, outer.offset() + 1);
|
||||||
|
int childEnd = Math.addExact(child.offset(), child.length());
|
||||||
|
if (childEnd > outerEnd) throw malformed();
|
||||||
|
return Arrays.copyOfRange(der, outer.offset(), childEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Length length(byte[] der, int offset) {
|
||||||
|
if (offset >= der.length) throw malformed();
|
||||||
|
int first = Byte.toUnsignedInt(der[offset]);
|
||||||
|
if (first < 128) return new Length(offset + 1, first);
|
||||||
|
int count = first & 0x7f;
|
||||||
|
if (count == 0 || count > 4 || offset + count >= der.length || der[offset + 1] == 0) throw malformed();
|
||||||
|
int value = 0;
|
||||||
|
for (int index = 0; index < count; index++) value = Math.addExact(Math.multiplyExact(value, 256),
|
||||||
|
Byte.toUnsignedInt(der[offset + index + 1]));
|
||||||
|
if (value < 128) throw malformed();
|
||||||
|
return new Length(offset + count + 1, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Length(int offset, int length) { }
|
||||||
|
|
||||||
private static IllegalArgumentException malformed() {
|
private static IllegalArgumentException malformed() {
|
||||||
return new IllegalArgumentException("OCSP request is malformed");
|
return new IllegalArgumentException("OCSP request is malformed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import java.security.SecureRandom;
|
|||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -75,7 +76,9 @@ import org.bouncycastle.asn1.x509.Extensions;
|
|||||||
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
|
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
|
||||||
import org.bouncycastle.asn1.x509.GeneralName;
|
import org.bouncycastle.asn1.x509.GeneralName;
|
||||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||||
|
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
|
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
|
||||||
import org.bouncycastle.cert.ocsp.CertificateID;
|
import org.bouncycastle.cert.ocsp.CertificateID;
|
||||||
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
|
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
|
||||||
@@ -90,6 +93,7 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
|
|
||||||
import zeroecho.core.storage.KeyringPassword;
|
import zeroecho.core.storage.KeyringPassword;
|
||||||
import zeroecho.core.storage.KeyringStore;
|
import zeroecho.core.storage.KeyringStore;
|
||||||
|
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||||
import zeroecho.pki.api.KeyRef;
|
import zeroecho.pki.api.KeyRef;
|
||||||
import zeroecho.pki.api.PkiId;
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.SubjectRef;
|
import zeroecho.pki.api.SubjectRef;
|
||||||
@@ -199,12 +203,18 @@ class AcmeEndToEndTest {
|
|||||||
assertOcspNonce(wire, publicPort, chain.get(1), chain.get(0));
|
assertOcspNonce(wire, publicPort, chain.get(1), chain.get(0));
|
||||||
assertOcspUnknownAndMulti(wire, publicPort, chain.get(1), chain.get(0));
|
assertOcspUnknownAndMulti(wire, publicPort, chain.get(1), chain.get(0));
|
||||||
assertOcspNoncePolicies(wire, publicPort, chain.get(1), chain.get(0));
|
assertOcspNoncePolicies(wire, publicPort, chain.get(1), chain.get(0));
|
||||||
|
assertSignedOcsp(wire, publicPort, chain.get(1), chain.get(0), rsa((byte) 81),
|
||||||
|
requesterCertificate(rsa((byte) 81)), "signed", true, false);
|
||||||
|
assertSignedOcsp(wire, publicPort, chain.get(1), chain.get(0), rsa((byte) 81),
|
||||||
|
requesterCertificate(rsa((byte) 81)), "mixed", false, false);
|
||||||
|
|
||||||
assertEquals(200, client.rollover(keyChange, replacementKey).status());
|
assertEquals(200, client.rollover(keyChange, replacementKey).status());
|
||||||
assertEquals(200, client.postAsGet(accountUri).status());
|
assertEquals(200, client.postAsGet(accountUri).status());
|
||||||
System.out.println("...https-jws-http01-issuance=true");
|
System.out.println("...https-jws-http01-issuance=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerManagedRequester(fixture, leafDer, leafKey);
|
||||||
|
|
||||||
try (PkiHttpsServer restarted = start(fixture);
|
try (PkiHttpsServer restarted = start(fixture);
|
||||||
AcmeTestClient replacement = new AcmeTestClient(wire, directoryUri, replacementKey);
|
AcmeTestClient replacement = new AcmeTestClient(wire, directoryUri, replacementKey);
|
||||||
AcmeTestClient old = new AcmeTestClient(wire, directoryUri, accountKey)) {
|
AcmeTestClient old = new AcmeTestClient(wire, directoryUri, accountKey)) {
|
||||||
@@ -215,12 +225,22 @@ class AcmeEndToEndTest {
|
|||||||
assertEquals(403, old.postAsGet(accountUri).status());
|
assertEquals(403, old.postAsGet(accountUri).status());
|
||||||
assertEquals(200, replacement.postAsGet(orderUri).status());
|
assertEquals(200, replacement.postAsGet(orderUri).status());
|
||||||
assertEquals(200, replacement.postAsGet(certificateUri).status());
|
assertEquals(200, replacement.postAsGet(certificateUri).status());
|
||||||
|
X509Certificate requesterCertificate = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(new ByteArrayInputStream(leafDer));
|
||||||
|
List<X509Certificate> preRevocationChain = certificates(
|
||||||
|
replacement.postAsGet(certificateUri).bodyText());
|
||||||
|
assertSignedOcsp(wire, publicPort, preRevocationChain.get(1), requesterCertificate, leafKey,
|
||||||
|
requesterCertificate, "managed", true, false);
|
||||||
URI revoke = URI.create(AcmeTestClient.text(replacement.directory(), "revokeCert"));
|
URI revoke = URI.create(AcmeTestClient.text(replacement.directory(), "revokeCert"));
|
||||||
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(leafDer);
|
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(leafDer);
|
||||||
assertEquals(200, replacement.kid(revoke,
|
assertEquals(200, replacement.kid(revoke,
|
||||||
"{\"certificate\":\"" + encoded + "\",\"reason\":1}").status());
|
"{\"certificate\":\"" + encoded + "\",\"reason\":1}").status());
|
||||||
List<X509Certificate> chain = certificates(replacement.postAsGet(certificateUri).bodyText());
|
List<X509Certificate> chain = certificates(replacement.postAsGet(certificateUri).bodyText());
|
||||||
assertOcsp(wire, publicPort, chain.get(1), chain.get(0), true);
|
assertOcsp(wire, publicPort, chain.get(1), chain.get(0), true);
|
||||||
|
assertSignedOcsp(wire, publicPort, chain.get(1), chain.get(0), rsa((byte) 81),
|
||||||
|
requesterCertificate(rsa((byte) 81)), "signed", true, true);
|
||||||
|
assertSignedOcspUnauthorized(wire, publicPort, chain.get(1), requesterCertificate, leafKey,
|
||||||
|
requesterCertificate, "managed");
|
||||||
assertFalse(replacement.kid(revoke,
|
assertFalse(replacement.kid(revoke,
|
||||||
"{\"certificate\":\"" + encoded + "\",\"reason\":1}").status() == 200);
|
"{\"certificate\":\"" + encoded + "\",\"reason\":1}").status() == 200);
|
||||||
System.out.println("...rollover-restart-revocation=true");
|
System.out.println("...rollover-restart-revocation=true");
|
||||||
@@ -258,6 +278,8 @@ class AcmeEndToEndTest {
|
|||||||
+ "/directory");
|
+ "/directory");
|
||||||
HttpClient wire = HttpClient.newBuilder().sslContext(tls.anonymousContext())
|
HttpClient wire = HttpClient.newBuilder().sslContext(tls.anonymousContext())
|
||||||
.connectTimeout(Duration.ofMillis(500)).build();
|
.connectTimeout(Duration.ofMillis(500)).build();
|
||||||
|
X509Certificate packagedIssuer = null;
|
||||||
|
X509Certificate packagedCertificate = null;
|
||||||
try {
|
try {
|
||||||
awaitDirectory(wire, directoryUri, process, output);
|
awaitDirectory(wire, directoryUri, process, output);
|
||||||
KeyPair accountKey = ec((byte) 42);
|
KeyPair accountKey = ec((byte) 42);
|
||||||
@@ -292,7 +314,11 @@ class AcmeEndToEndTest {
|
|||||||
URI certificate = URI.create(AcmeTestClient.text(finalized, "certificate"));
|
URI certificate = URI.create(AcmeTestClient.text(finalized, "certificate"));
|
||||||
List<X509Certificate> chain = certificates(client.postAsGet(certificate).bodyText());
|
List<X509Certificate> chain = certificates(client.postAsGet(certificate).bodyText());
|
||||||
assertEquals(2, chain.size());
|
assertEquals(2, chain.size());
|
||||||
|
packagedCertificate = chain.get(0);
|
||||||
|
packagedIssuer = chain.get(1);
|
||||||
assertOcsp(wire, publicPort, chain.get(1), chain.get(0), false);
|
assertOcsp(wire, publicPort, chain.get(1), chain.get(0), false);
|
||||||
|
assertSignedOcsp(wire, publicPort, chain.get(1), chain.get(0), rsa((byte) 81),
|
||||||
|
requesterCertificate(rsa((byte) 81)), "signed", true, false);
|
||||||
assertEquals(200, client.rollover(rollover, replacement).status());
|
assertEquals(200, client.rollover(rollover, replacement).status());
|
||||||
String der = Base64.getUrlEncoder().withoutPadding().encodeToString(chain.get(0).getEncoded());
|
String der = Base64.getUrlEncoder().withoutPadding().encodeToString(chain.get(0).getEncoded());
|
||||||
assertEquals(200, client.kid(revoke,
|
assertEquals(200, client.kid(revoke,
|
||||||
@@ -307,6 +333,21 @@ class AcmeEndToEndTest {
|
|||||||
assertTrue(process.waitFor(5, TimeUnit.SECONDS));
|
assertTrue(process.waitFor(5, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
denyPackagedSignedRequester(fixture);
|
||||||
|
Process deniedProcess = builder.start();
|
||||||
|
try {
|
||||||
|
awaitDirectory(wire, directoryUri, deniedProcess, output);
|
||||||
|
assertSignedOcspUnauthorized(wire, publicPort,
|
||||||
|
java.util.Objects.requireNonNull(packagedIssuer),
|
||||||
|
java.util.Objects.requireNonNull(packagedCertificate), rsa((byte) 81),
|
||||||
|
requesterCertificate(rsa((byte) 81)), "signed");
|
||||||
|
} finally {
|
||||||
|
deniedProcess.destroy();
|
||||||
|
if (!deniedProcess.waitFor(10, TimeUnit.SECONDS)) {
|
||||||
|
deniedProcess.destroyForcibly();
|
||||||
|
assertTrue(deniedProcess.waitFor(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
System.out.println("...ok");
|
System.out.println("...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +448,7 @@ class AcmeEndToEndTest {
|
|||||||
zeroecho.pki.application.OcspResponseService.CertIdHash.SHA256),
|
zeroecho.pki.application.OcspResponseService.CertIdHash.SHA256),
|
||||||
16_384, 8, Duration.ofMinutes(1));
|
16_384, 8, Duration.ofMinutes(1));
|
||||||
context.ocspResponders().setActive(context.ocspResponders().register(responder).responderId(), true);
|
context.ocspResponders().setActive(context.ocspResponders().register(responder).responderId(), true);
|
||||||
|
registerSignedResponder(context, responder, authority, issuer.issuerId());
|
||||||
registerNonceResponder(context, responder, "ocsp-e2e-reject", "reject",
|
registerNonceResponder(context, responder, "ocsp-e2e-reject", "reject",
|
||||||
OcspResponderService.NoncePolicy.REJECT);
|
OcspResponderService.NoncePolicy.REJECT);
|
||||||
registerNonceResponder(context, responder, "ocsp-e2e-required", "required",
|
registerNonceResponder(context, responder, "ocsp-e2e-required", "required",
|
||||||
@@ -448,6 +490,162 @@ class AcmeEndToEndTest {
|
|||||||
context.ocspResponders().setActive(context.ocspResponders().register(responder).responderId(), true);
|
context.ocspResponders().setActive(context.ocspResponders().register(responder).responderId(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void registerManagedRequester(Fixture fixture, byte[] requesterDer, KeyPair requesterKey)
|
||||||
|
throws Exception {
|
||||||
|
X509Certificate requester = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(new ByteArrayInputStream(requesterDer));
|
||||||
|
try (ServerRealmContext context = ServerRealmContext.open(fixture.configuration().realm(),
|
||||||
|
fixture.dependencies(), ServerTestSupport.CLOCK, HttpServerTestSupport.random())) {
|
||||||
|
OcspResponderService.Responder template = context.ocspResponders().requireAlias("root");
|
||||||
|
String commitment = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(requesterDer));
|
||||||
|
context.createPrincipal(new SecurityPrincipal("managed-ocsp-requester", SecurityPrincipal.Type.SERVICE,
|
||||||
|
"Managed OCSP requester", Optional.empty(), Map.of(), true), "system");
|
||||||
|
Permission.Scope scope = new Permission.Scope(context.configuration().realmId(),
|
||||||
|
Optional.of(template.authorityId()), Optional.of(template.issuerId()), Optional.empty(),
|
||||||
|
Optional.of("ocsp-e2e-managed"));
|
||||||
|
context.grant(new Permission.Grant("ocsp-query-managed-e2e", "managed-ocsp-requester",
|
||||||
|
Permission.Effect.ALLOW, Permission.Action.OCSP_STATUS_QUERY,
|
||||||
|
Permission.ResourceType.OCSP_RESPONDER, scope, Permission.Relationship.ANY,
|
||||||
|
Permission.DataView.METADATA_REDACTED, Set.of(), Optional.empty(), true), "system");
|
||||||
|
zeroecho.pki.api.algorithm.X509AlgorithmBinding binding = context.session().algorithmBindings()
|
||||||
|
.standard(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256,
|
||||||
|
zeroecho.pki.api.algorithm.X509AlgorithmBinding.Role.OCSP_REQUEST_SIGNATURE)
|
||||||
|
.orElseThrow();
|
||||||
|
OcspResponderService.RequesterTrustPolicy policy = new OcspResponderService.RequesterTrustPolicy(
|
||||||
|
List.of(new OcspResponderService.TrustSelector(
|
||||||
|
OcspResponderService.TrustSelectorType.MANAGED_ISSUER_GENERATIONS,
|
||||||
|
Set.of(template.issuerId().value()))),
|
||||||
|
List.of(new OcspResponderService.PrincipalMapping("managed-requester-cert",
|
||||||
|
"managed-ocsp-requester",
|
||||||
|
OcspResponderService.PrincipalMappingType.SHA256_CERTIFICATE_COMMITMENT, commitment)),
|
||||||
|
List.of(new OcspResponderService.RequestSignatureBinding(binding.bindingId(),
|
||||||
|
binding.semanticCommitment())),
|
||||||
|
OcspResponderService.RequesterRevocationPolicy.LOCAL_AUTHORITATIVE_REQUIRED, 3, 131_072);
|
||||||
|
assertTrue(context.session().repository().credential(template.issuerId(), requester.getSerialNumber())
|
||||||
|
.isPresent());
|
||||||
|
OcspResponderService.Responder responder = context.ocspResponders().create("ocsp-e2e-managed",
|
||||||
|
"managed", template.authorityId(), template.issuerId(), template.signingMode(),
|
||||||
|
template.responderCredentialId(), template.signingKeyRef(), template.chainPathId(),
|
||||||
|
template.signatureAlgorithm(), template.signatureBindingId(),
|
||||||
|
template.signatureBindingCommitment(), template.responderIdForm(), template.responseValidity(),
|
||||||
|
template.noncePolicy(), template.maximumNonceBytes(), template.acceptedHashes(),
|
||||||
|
template.maximumRequestBytes(), template.maximumEntries(), template.cacheLifetime(),
|
||||||
|
OcspResponderService.RequestAuthenticationMode.AUTHORIZED_SIGNED_REQUIRED,
|
||||||
|
Optional.of(policy));
|
||||||
|
OcspResponderService.Responder active = context.ocspResponders().setActive(
|
||||||
|
context.ocspResponders().register(responder).responderId(), true);
|
||||||
|
assertRequesterService(context, active, requesterKey, requester);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void denyPackagedSignedRequester(Fixture fixture) throws Exception {
|
||||||
|
try (ServerRealmContext context = ServerRealmContext.open(fixture.configuration().realm(),
|
||||||
|
fixture.dependencies(), ServerTestSupport.CLOCK, HttpServerTestSupport.random())) {
|
||||||
|
OcspResponderService.Responder responder = context.ocspResponders().requireAlias("signed");
|
||||||
|
Permission.Scope scope = new Permission.Scope(context.configuration().realmId(),
|
||||||
|
Optional.of(responder.authorityId()), Optional.of(responder.issuerId()), Optional.empty(),
|
||||||
|
Optional.of(responder.responderId()));
|
||||||
|
context.grant(new Permission.Grant("ocsp-query-e2e-deny", "ocsp-requester",
|
||||||
|
Permission.Effect.DENY, Permission.Action.OCSP_STATUS_QUERY,
|
||||||
|
Permission.ResourceType.OCSP_RESPONDER, scope, Permission.Relationship.ANY,
|
||||||
|
Permission.DataView.METADATA_REDACTED, Set.of(), Optional.empty(), true), "system");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerSignedResponder(ServerRealmContext context,
|
||||||
|
OcspResponderService.Responder template, PkiId authority, PkiId issuerId) throws Exception {
|
||||||
|
KeyPair requesterKey = rsa((byte) 81);
|
||||||
|
X509Certificate requesterCertificate = requesterCertificate(requesterKey);
|
||||||
|
String certificateCommitment = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(requesterCertificate.getEncoded()));
|
||||||
|
context.createPrincipal(new SecurityPrincipal("ocsp-requester", SecurityPrincipal.Type.SERVICE,
|
||||||
|
"OCSP requester", Optional.empty(), Map.of(), true), "system");
|
||||||
|
Permission.Scope scope = new Permission.Scope(context.configuration().realmId(), Optional.of(authority),
|
||||||
|
Optional.of(issuerId), Optional.empty(), Optional.of("ocsp-e2e-signed"));
|
||||||
|
context.grant(new Permission.Grant("ocsp-query-e2e", "ocsp-requester", Permission.Effect.ALLOW,
|
||||||
|
Permission.Action.OCSP_STATUS_QUERY, Permission.ResourceType.OCSP_RESPONDER, scope,
|
||||||
|
Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, Set.of(), Optional.empty(),
|
||||||
|
true), "system");
|
||||||
|
Permission.Scope mixedScope = new Permission.Scope(context.configuration().realmId(),
|
||||||
|
Optional.of(authority), Optional.of(issuerId), Optional.empty(), Optional.of("ocsp-e2e-mixed"));
|
||||||
|
context.grant(new Permission.Grant("ocsp-query-mixed-e2e", "ocsp-requester", Permission.Effect.ALLOW,
|
||||||
|
Permission.Action.OCSP_STATUS_QUERY, Permission.ResourceType.OCSP_RESPONDER, mixedScope,
|
||||||
|
Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, Set.of(), Optional.empty(),
|
||||||
|
true), "system");
|
||||||
|
zeroecho.pki.api.algorithm.X509AlgorithmBinding requestBinding = context.session().algorithmBindings()
|
||||||
|
.standard(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256,
|
||||||
|
zeroecho.pki.api.algorithm.X509AlgorithmBinding.Role.OCSP_REQUEST_SIGNATURE).orElseThrow();
|
||||||
|
OcspResponderService.RequesterTrustPolicy policy = new OcspResponderService.RequesterTrustPolicy(
|
||||||
|
List.of(new OcspResponderService.TrustSelector(
|
||||||
|
OcspResponderService.TrustSelectorType.EXACT_REQUESTER_CERTIFICATE_COMMITMENTS,
|
||||||
|
Set.of(certificateCommitment))),
|
||||||
|
List.of(new OcspResponderService.PrincipalMapping("ocsp-requester-cert", "ocsp-requester",
|
||||||
|
OcspResponderService.PrincipalMappingType.SHA256_CERTIFICATE_COMMITMENT,
|
||||||
|
certificateCommitment)),
|
||||||
|
List.of(new OcspResponderService.RequestSignatureBinding(requestBinding.bindingId(),
|
||||||
|
requestBinding.semanticCommitment())),
|
||||||
|
OcspResponderService.RequesterRevocationPolicy.VALIDITY_AND_TRUST_ONLY, 2, 65_536);
|
||||||
|
OcspResponderService.Responder responder = context.ocspResponders().create("ocsp-e2e-signed", "signed",
|
||||||
|
authority, issuerId, template.signingMode(), template.responderCredentialId(),
|
||||||
|
template.signingKeyRef(), template.chainPathId(), template.signatureAlgorithm(),
|
||||||
|
template.signatureBindingId(), template.signatureBindingCommitment(), template.responderIdForm(),
|
||||||
|
template.responseValidity(), template.noncePolicy(), template.maximumNonceBytes(),
|
||||||
|
template.acceptedHashes(), template.maximumRequestBytes(), template.maximumEntries(),
|
||||||
|
template.cacheLifetime(), OcspResponderService.RequestAuthenticationMode.AUTHORIZED_SIGNED_REQUIRED,
|
||||||
|
Optional.of(policy));
|
||||||
|
OcspResponderService.Responder active = context.ocspResponders().setActive(
|
||||||
|
context.ocspResponders().register(responder).responderId(), true);
|
||||||
|
assertTrue(context.authorizeOcspQuery("ocsp-requester", active).allowed());
|
||||||
|
assertRequesterService(context, active, requesterKey, requesterCertificate);
|
||||||
|
OcspResponderService.Responder mixed = context.ocspResponders().create("ocsp-e2e-mixed", "mixed",
|
||||||
|
authority, issuerId, template.signingMode(), template.responderCredentialId(),
|
||||||
|
template.signingKeyRef(), template.chainPathId(), template.signatureAlgorithm(),
|
||||||
|
template.signatureBindingId(), template.signatureBindingCommitment(), template.responderIdForm(),
|
||||||
|
template.responseValidity(), template.noncePolicy(), template.maximumNonceBytes(),
|
||||||
|
template.acceptedHashes(), template.maximumRequestBytes(), template.maximumEntries(),
|
||||||
|
template.cacheLifetime(),
|
||||||
|
OcspResponderService.RequestAuthenticationMode.UNSIGNED_OR_AUTHORIZED_SIGNED,
|
||||||
|
Optional.of(policy));
|
||||||
|
context.ocspResponders().setActive(context.ocspResponders().register(mixed).responderId(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRequesterService(ServerRealmContext context,
|
||||||
|
OcspResponderService.Responder responder, KeyPair requesterKey, X509Certificate requesterCertificate)
|
||||||
|
throws Exception {
|
||||||
|
X509Certificate issuerCertificate;
|
||||||
|
try (zeroecho.pki.application.PkiRepositoryContent content = context.session().repository()
|
||||||
|
.openCredential(context.session().repository().issuer(responder.issuerId()).orElseThrow()
|
||||||
|
.credentialId()); java.io.InputStream input = content.openStream()) {
|
||||||
|
issuerCertificate = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(input);
|
||||||
|
}
|
||||||
|
OCSPReqBuilder builder = new OCSPReqBuilder();
|
||||||
|
builder.addRequest(new CertificateID(new JcaDigestCalculatorProviderBuilder().build()
|
||||||
|
.get(CertificateID.HASH_SHA1), new JcaX509CertificateHolder(issuerCertificate), BigInteger.ONE));
|
||||||
|
builder.setRequestorName(new GeneralName(new JcaX509CertificateHolder(requesterCertificate).getSubject()));
|
||||||
|
org.bouncycastle.cert.ocsp.OCSPReq request = builder.build(new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.build(requesterKey.getPrivate()), new org.bouncycastle.cert.X509CertificateHolder[] {
|
||||||
|
new JcaX509CertificateHolder(requesterCertificate) });
|
||||||
|
org.bouncycastle.asn1.ocsp.OCSPRequest structure = org.bouncycastle.asn1.ocsp.OCSPRequest.getInstance(
|
||||||
|
org.bouncycastle.asn1.ASN1Primitive.fromByteArray(request.getEncoded()));
|
||||||
|
org.bouncycastle.asn1.ocsp.Signature signature = structure.getOptionalSignature();
|
||||||
|
org.bouncycastle.cert.ocsp.CertificateID certificateId = request.getRequestList()[0].getCertID();
|
||||||
|
OcspRequesterService.SignedRequest signed = new OcspRequesterService.SignedRequest(
|
||||||
|
structure.getTbsRequest().getEncoded(), requesterCertificate.getSubjectX500Principal().getEncoded(),
|
||||||
|
signature.getSignatureAlgorithm().getEncoded(), signature.getSignature().getOctets(),
|
||||||
|
List.of(requesterCertificate.getEncoded()));
|
||||||
|
try {
|
||||||
|
new OcspRequesterService(context, ServerTestSupport.CLOCK).authenticate(responder, signed,
|
||||||
|
List.of(new zeroecho.pki.application.OcspResponseService.CertId(
|
||||||
|
zeroecho.pki.application.OcspResponseService.CertIdHash.SHA1,
|
||||||
|
certificateId.getIssuerNameHash(), certificateId.getIssuerKeyHash(),
|
||||||
|
certificateId.getSerialNumber())));
|
||||||
|
} catch (OcspRequesterService.UnauthorizedException failure) {
|
||||||
|
throw new AssertionError("Requester service rejected fixture: " + failure.classification(), failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PkiHttpsServer start(Fixture fixture) throws Exception {
|
private PkiHttpsServer start(Fixture fixture) throws Exception {
|
||||||
return PkiHttpsServer.start(fixture.configuration(), fixture.dependencies(), ServerTestSupport.CLOCK,
|
return PkiHttpsServer.start(fixture.configuration(), fixture.dependencies(), ServerTestSupport.CLOCK,
|
||||||
HttpServerTestSupport.random(), TestTlsProvider.class.getClassLoader());
|
HttpServerTestSupport.random(), TestTlsProvider.class.getClassLoader());
|
||||||
@@ -766,6 +964,112 @@ class AcmeEndToEndTest {
|
|||||||
postOcsp(client, publicPort, "required", withoutNonce).getStatus());
|
postOcsp(client, publicPort, "required", withoutNonce).getStatus());
|
||||||
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.SUCCESSFUL,
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.SUCCESSFUL,
|
||||||
postOcsp(client, publicPort, "required", withNonce).getStatus());
|
postOcsp(client, publicPort, "required", withNonce).getStatus());
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.MALFORMED_REQUEST,
|
||||||
|
postOcsp(client, publicPort, "required", withNonce).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSignedOcsp(HttpClient client, int publicPort, X509Certificate issuer,
|
||||||
|
X509Certificate certificate, KeyPair requesterKey, X509Certificate requesterCertificate,
|
||||||
|
String alias, boolean signatureRequired, boolean revoked)
|
||||||
|
throws Exception {
|
||||||
|
JcaX509CertificateHolder issuerHolder = new JcaX509CertificateHolder(issuer);
|
||||||
|
OCSPReqBuilder builder = new OCSPReqBuilder();
|
||||||
|
builder.addRequest(new CertificateID(new JcaDigestCalculatorProviderBuilder().build()
|
||||||
|
.get(CertificateID.HASH_SHA1), issuerHolder, certificate.getSerialNumber()));
|
||||||
|
builder.setRequestorName(new GeneralName(new JcaX509CertificateHolder(requesterCertificate).getSubject()));
|
||||||
|
byte[] request = builder.build(new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.build(requesterKey.getPrivate()),
|
||||||
|
new org.bouncycastle.cert.X509CertificateHolder[] {
|
||||||
|
new JcaX509CertificateHolder(requesterCertificate) }).getEncoded();
|
||||||
|
URI endpoint = URI.create("https://localhost:" + publicPort + "/ocsp/" + alias);
|
||||||
|
java.net.http.HttpResponse<byte[]> post = client.send(java.net.http.HttpRequest.newBuilder(endpoint)
|
||||||
|
.header("Accept", "application/ocsp-response").header("Content-Type", "application/ocsp-request")
|
||||||
|
.POST(java.net.http.HttpRequest.BodyPublishers.ofByteArray(request)).build(),
|
||||||
|
java.net.http.HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
assertEquals(200, post.statusCode());
|
||||||
|
OCSPResp decoded = new OCSPResp(post.body());
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.SUCCESSFUL, decoded.getStatus());
|
||||||
|
BasicOCSPResp basic = assertInstanceOf(BasicOCSPResp.class, decoded.getResponseObject());
|
||||||
|
assertEquals(1, basic.getResponses().length);
|
||||||
|
if (revoked) assertInstanceOf(org.bouncycastle.cert.ocsp.RevokedStatus.class,
|
||||||
|
basic.getResponses()[0].getCertStatus());
|
||||||
|
else assertEquals(null, basic.getResponses()[0].getCertStatus());
|
||||||
|
assertEquals("private, no-store", post.headers().firstValue("Cache-Control").orElseThrow());
|
||||||
|
assertEquals("no-cache", post.headers().firstValue("Pragma").orElseThrow());
|
||||||
|
assertTrue(post.headers().firstValue("ETag").isEmpty());
|
||||||
|
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(request);
|
||||||
|
java.net.http.HttpResponse<byte[]> get = client.send(java.net.http.HttpRequest.newBuilder(
|
||||||
|
URI.create(endpoint + "/" + encoded)).header("Accept", "application/ocsp-response").GET().build(),
|
||||||
|
java.net.http.HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.SUCCESSFUL, new OCSPResp(get.body()).getStatus());
|
||||||
|
OCSPReqBuilder unsigned = new OCSPReqBuilder();
|
||||||
|
unsigned.addRequest(new CertificateID(new JcaDigestCalculatorProviderBuilder().build()
|
||||||
|
.get(CertificateID.HASH_SHA1), issuerHolder, certificate.getSerialNumber()));
|
||||||
|
assertEquals(signatureRequired ? org.bouncycastle.cert.ocsp.OCSPRespBuilder.SIG_REQUIRED
|
||||||
|
: org.bouncycastle.cert.ocsp.OCSPRespBuilder.SUCCESSFUL,
|
||||||
|
postOcsp(client, publicPort, alias, unsigned.build().getEncoded()).getStatus());
|
||||||
|
if ("mixed".equals(alias)) {
|
||||||
|
byte[] invalidSignature = builder.build(new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.build(rsa((byte) 82).getPrivate()),
|
||||||
|
new org.bouncycastle.cert.X509CertificateHolder[] {
|
||||||
|
new JcaX509CertificateHolder(requesterCertificate) }).getEncoded();
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.UNAUTHORIZED,
|
||||||
|
postOcsp(client, publicPort, alias, invalidSignature).getStatus());
|
||||||
|
|
||||||
|
OCSPReqBuilder partlyForeign = new OCSPReqBuilder();
|
||||||
|
partlyForeign.addRequest(new CertificateID(new JcaDigestCalculatorProviderBuilder().build()
|
||||||
|
.get(CertificateID.HASH_SHA1), issuerHolder, certificate.getSerialNumber()));
|
||||||
|
partlyForeign.addRequest(new CertificateID(new JcaDigestCalculatorProviderBuilder().build()
|
||||||
|
.get(CertificateID.HASH_SHA1), new JcaX509CertificateHolder(requesterCertificate),
|
||||||
|
certificate.getSerialNumber().add(BigInteger.ONE)));
|
||||||
|
partlyForeign.setRequestorName(new GeneralName(
|
||||||
|
new JcaX509CertificateHolder(requesterCertificate).getSubject()));
|
||||||
|
byte[] partlyForeignRequest = partlyForeign.build(new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.build(requesterKey.getPrivate()),
|
||||||
|
new org.bouncycastle.cert.X509CertificateHolder[] {
|
||||||
|
new JcaX509CertificateHolder(requesterCertificate) }).getEncoded();
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.UNAUTHORIZED,
|
||||||
|
postOcsp(client, publicPort, alias, partlyForeignRequest).getStatus());
|
||||||
|
}
|
||||||
|
if (signatureRequired) {
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.UNAUTHORIZED,
|
||||||
|
postOcsp(client, publicPort, "root", request).getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSignedOcspUnauthorized(HttpClient client, int publicPort, X509Certificate issuer,
|
||||||
|
X509Certificate certificate, KeyPair requesterKey, X509Certificate requesterCertificate,
|
||||||
|
String alias) throws Exception {
|
||||||
|
OCSPReqBuilder builder = new OCSPReqBuilder();
|
||||||
|
builder.addRequest(new CertificateID(new JcaDigestCalculatorProviderBuilder().build()
|
||||||
|
.get(CertificateID.HASH_SHA1), new JcaX509CertificateHolder(issuer),
|
||||||
|
certificate.getSerialNumber()));
|
||||||
|
builder.setRequestorName(new GeneralName(new JcaX509CertificateHolder(requesterCertificate).getSubject()));
|
||||||
|
byte[] request = builder.build(new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.build(requesterKey.getPrivate()), new org.bouncycastle.cert.X509CertificateHolder[] {
|
||||||
|
new JcaX509CertificateHolder(requesterCertificate) }).getEncoded();
|
||||||
|
java.net.http.HttpResponse<byte[]> response = client.send(java.net.http.HttpRequest.newBuilder(
|
||||||
|
URI.create("https://localhost:" + publicPort + "/ocsp/" + alias))
|
||||||
|
.header("Accept", "application/ocsp-response").header("Content-Type", "application/ocsp-request")
|
||||||
|
.POST(java.net.http.HttpRequest.BodyPublishers.ofByteArray(request)).build(),
|
||||||
|
java.net.http.HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
assertEquals(200, response.statusCode());
|
||||||
|
assertEquals("no-store", response.headers().firstValue("Cache-Control").orElseThrow());
|
||||||
|
assertEquals(org.bouncycastle.cert.ocsp.OCSPRespBuilder.UNAUTHORIZED,
|
||||||
|
new OCSPResp(response.body()).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate requesterCertificate(KeyPair keys) throws Exception {
|
||||||
|
X500Name name = new X500Name("CN=ZeroEcho OCSP Requester Fixture");
|
||||||
|
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(name, BigInteger.valueOf(8801),
|
||||||
|
java.util.Date.from(Instant.parse("2025-01-01T00:00:00Z")),
|
||||||
|
java.util.Date.from(Instant.parse("2035-01-01T00:00:00Z")), name,
|
||||||
|
org.bouncycastle.asn1.x509.SubjectPublicKeyInfo.getInstance(keys.getPublic().getEncoded()));
|
||||||
|
builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature));
|
||||||
|
byte[] encoded = builder.build(new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.build(keys.getPrivate())).getEncoded();
|
||||||
|
return (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(new ByteArrayInputStream(encoded));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OCSPResp postOcsp(HttpClient client, int publicPort, String alias, byte[] request)
|
private static OCSPResp postOcsp(HttpClient client, int publicPort, String alias, byte[] request)
|
||||||
|
|||||||
@@ -104,6 +104,44 @@ class AuthorizationAndRoleTest {
|
|||||||
System.out.println("...ok");
|
System.out.println("...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scopesOcspQuerySeparatelyFromResponderAdministration() {
|
||||||
|
System.out.println("scopesOcspQuerySeparatelyFromResponderAdministration");
|
||||||
|
AuthorizationEngine engine = new AuthorizationEngine(ServerTestSupport.CLOCK);
|
||||||
|
SecurityPrincipal principal = ServerTestSupport.principal("ocsp-client");
|
||||||
|
Permission.Scope exact = new Permission.Scope(ServerTestSupport.REALM,
|
||||||
|
Optional.of(ServerTestSupport.AUTHORITY), Optional.of(new zeroecho.pki.api.PkiId("issuer-a")),
|
||||||
|
Optional.empty(), Optional.of("responder-a"));
|
||||||
|
Permission.Resource resource = new Permission.Resource(Permission.ResourceType.OCSP_RESPONDER, exact,
|
||||||
|
Optional.empty(), Optional.empty());
|
||||||
|
Permission.Grant allow = new Permission.Grant("ocsp-allow", principal.principalId(),
|
||||||
|
Permission.Effect.ALLOW, Permission.Action.OCSP_STATUS_QUERY,
|
||||||
|
Permission.ResourceType.OCSP_RESPONDER, exact, Permission.Relationship.ANY,
|
||||||
|
Permission.DataView.METADATA_REDACTED, Set.of(), Optional.empty(), true);
|
||||||
|
Permission.Grant administer = new Permission.Grant("ocsp-admin", principal.principalId(),
|
||||||
|
Permission.Effect.ALLOW, Permission.Action.OCSP_ADMINISTER,
|
||||||
|
Permission.ResourceType.OCSP_RESPONDER, exact, Permission.Relationship.ANY,
|
||||||
|
Permission.DataView.METADATA_REDACTED, Set.of(), Optional.empty(), true);
|
||||||
|
AuthorizationEngine.Request request = new AuthorizationEngine.Request(ServerTestSupport.REALM,
|
||||||
|
new AuthorityExposurePolicy(AuthorityExposurePolicy.Mode.ALL_REALM_AUTHORITIES, Set.of(), false),
|
||||||
|
principal, Permission.Action.OCSP_STATUS_QUERY, resource, Permission.Relationship.ANY,
|
||||||
|
Permission.DataView.METADATA_REDACTED, Permission.Context.empty(), List.of(allow), Set.of());
|
||||||
|
assertTrue(engine.authorize(request).allowed());
|
||||||
|
assertEquals(AuthorizationEngine.Code.NO_MATCHING_GRANT, engine.authorize(new AuthorizationEngine.Request(
|
||||||
|
request.realmId(), request.exposure(), principal, request.action(), resource,
|
||||||
|
request.relationship(), request.dataView(), request.context(), List.of(administer), Set.of())).code());
|
||||||
|
Permission.Scope otherResponder = new Permission.Scope(ServerTestSupport.REALM,
|
||||||
|
Optional.of(ServerTestSupport.AUTHORITY), Optional.of(new zeroecho.pki.api.PkiId("issuer-a")),
|
||||||
|
Optional.empty(), Optional.of("responder-b"));
|
||||||
|
assertEquals(AuthorizationEngine.Code.NO_MATCHING_GRANT, engine.authorize(new AuthorizationEngine.Request(
|
||||||
|
request.realmId(), request.exposure(), principal, request.action(),
|
||||||
|
new Permission.Resource(Permission.ResourceType.OCSP_RESPONDER, otherResponder,
|
||||||
|
Optional.empty(), Optional.empty()), request.relationship(), request.dataView(),
|
||||||
|
request.context(), List.of(allow), Set.of())).code());
|
||||||
|
System.out.println("...query-admin-separation=true responder-scope=true");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadsExactTemplatesAndRejectsUnknownSchemaFields() throws Exception {
|
void loadsExactTemplatesAndRejectsUnknownSchemaFields() throws Exception {
|
||||||
System.out.println("loadsExactTemplatesAndRejectsUnknownSchemaFields");
|
System.out.println("loadsExactTemplatesAndRejectsUnknownSchemaFields");
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ package zeroecho.pki.server.http;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -119,8 +120,8 @@ class OcspRequestParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsSignedRequestsWithoutTreatingSignaturesAsAuthority() throws Exception {
|
void retainsExactSignedRequestAndRequiresExplicitSignerBounds() throws Exception {
|
||||||
System.out.println("rejectsSignedRequestsWithoutTreatingSignaturesAsAuthority");
|
System.out.println("retainsExactSignedRequestAndRequiresExplicitSignerBounds");
|
||||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||||
generator.initialize(2048);
|
generator.initialize(2048);
|
||||||
KeyPair keys = generator.generateKeyPair();
|
KeyPair keys = generator.generateKeyPair();
|
||||||
@@ -133,8 +134,16 @@ class OcspRequestParserTest {
|
|||||||
new X509CertificateHolder[] { issuer }).getEncoded();
|
new X509CertificateHolder[] { issuer }).getEncoded();
|
||||||
assertThrows(IllegalArgumentException.class, () -> OcspRequestParser.parse(signed, 1,
|
assertThrows(IllegalArgumentException.class, () -> OcspRequestParser.parse(signed, 1,
|
||||||
Set.of(OcspResponseService.CertIdHash.SHA1), 32));
|
Set.of(OcspResponseService.CertIdHash.SHA1), 32));
|
||||||
System.out.println("...signed-request-authority=false");
|
OcspRequestParser.Parsed parsed = OcspRequestParser.parse(signed, 1,
|
||||||
System.out.println("rejectsSignedRequestsWithoutTreatingSignaturesAsAuthority...ok");
|
Set.of(OcspResponseService.CertIdHash.SHA1), 32, 2, 32_768);
|
||||||
|
assertTrue(parsed.signedRequest().isPresent());
|
||||||
|
assertArrayEquals(issuer.getSubject().getEncoded(),
|
||||||
|
parsed.signedRequest().orElseThrow().requestorName());
|
||||||
|
assertArrayEquals(issuer.getEncoded(),
|
||||||
|
parsed.signedRequest().orElseThrow().certificates().getFirst());
|
||||||
|
assertTrue(parsed.signedRequest().orElseThrow().tbsRequest().length > 0);
|
||||||
|
System.out.println("...exact-tbs-and-certificate-retained=true");
|
||||||
|
System.out.println("retainsExactSignedRequestAndRequiresExplicitSignerBounds...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static X509CertificateHolder certificate() throws Exception {
|
private static X509CertificateHolder certificate() throws Exception {
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ public record X509AlgorithmBinding(String bindingId, AlgorithmIdentity algorithm
|
|||||||
/** Certificate signature. */
|
/** Certificate signature. */
|
||||||
CERTIFICATE_SIGNATURE,
|
CERTIFICATE_SIGNATURE,
|
||||||
/** Certificate-revocation-list signature. */
|
/** Certificate-revocation-list signature. */
|
||||||
CRL_SIGNATURE
|
CRL_SIGNATURE,
|
||||||
|
/** Signed OCSP request signature. */
|
||||||
|
OCSP_REQUEST_SIGNATURE
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Authority that owns the OID assignment. */
|
/** Authority that owns the OID assignment. */
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ public final class StandardX509Bindings {
|
|||||||
Optional.empty(), commitment));
|
Optional.empty(), commitment));
|
||||||
}
|
}
|
||||||
return List.of(X509AlgorithmBinding.Role.CSR_SIGNATURE, X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE,
|
return List.of(X509AlgorithmBinding.Role.CSR_SIGNATURE, X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE,
|
||||||
X509AlgorithmBinding.Role.CRL_SIGNATURE).stream()
|
X509AlgorithmBinding.Role.CRL_SIGNATURE, X509AlgorithmBinding.Role.OCSP_REQUEST_SIGNATURE).stream()
|
||||||
.map(bindingRole -> new X509AlgorithmBinding(id + "."
|
.map(bindingRole -> new X509AlgorithmBinding(id + "."
|
||||||
+ bindingRole.name().toLowerCase(java.util.Locale.ROOT), identity,
|
+ bindingRole.name().toLowerCase(java.util.Locale.ROOT), identity,
|
||||||
bindingRole, identifier.oid(), X509AlgorithmBinding.Origin.STANDARD, 1, parameters, keyEncoding,
|
bindingRole, identifier.oid(), X509AlgorithmBinding.Origin.STANDARD, 1, parameters, keyEncoding,
|
||||||
|
|||||||
Reference in New Issue
Block a user