Remove java.util.Date from VerifiableCredential (#30920)

closes #30918

Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de>
Signed-off-by: Captain-P-Goldfish <captain.p.goldfish@gmx.de>
This commit is contained in:
Pascal Knüppel 2024-07-18 09:52:02 +02:00 committed by GitHub
parent 526286e851
commit 018a0802bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 54 additions and 60 deletions

View file

@ -54,6 +54,10 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>

View file

@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.io.InputStream;
@ -43,6 +44,7 @@ public class JsonSerialization {
static {
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
prettyMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

View file

@ -462,7 +462,7 @@ public class OID4VCIssuerEndpoint {
// set the required claims
VerifiableCredential vc = new VerifiableCredential()
.setIssuer(URI.create(issuerDid))
.setIssuanceDate(Date.from(Instant.ofEpochMilli(timeProvider.currentTimeMillis())))
.setIssuanceDate(Instant.ofEpochMilli(timeProvider.currentTimeMillis()))
.setType(List.of(vcType));
Map<String, Object> subjectClaims = new HashMap<>();

View file

@ -28,6 +28,7 @@ import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.representations.JsonWebToken;
import java.net.URI;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
@ -74,7 +75,7 @@ public class JwtSigningService extends SigningService<String> {
// Get the issuance date from the credential. Since nbf is mandatory, we set it to the current time if not
// provided
long iat = Optional.ofNullable(verifiableCredential.getIssuanceDate())
.map(issuanceDate -> issuanceDate.toInstant().getEpochSecond())
.map(Instant::getEpochSecond)
.orElse((long) timeProvider.currentTimeSeconds());
// set mandatory fields
@ -86,7 +87,7 @@ public class JwtSigningService extends SigningService<String> {
// expiry is optional
Optional.ofNullable(verifiableCredential.getExpirationDate())
.ifPresent(d -> jsonWebToken.exp(d.toInstant().getEpochSecond()));
.ifPresent(d -> jsonWebToken.exp(d.getEpochSecond()));
// subject id should only be set if the credential subject has an id.
Optional.ofNullable(
@ -110,4 +111,4 @@ public class JwtSigningService extends SigningService<String> {
.orElse(URI.create(String.format(ID_TEMPLATE, UUID.randomUUID())))
.toString();
}
}
}

View file

@ -18,7 +18,6 @@
package org.keycloak.protocol.oid4vc.issuance.signing;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.common.util.Base64;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureProvider;
@ -48,7 +47,7 @@ public class LDSigningService extends SigningService<VerifiableCredential> {
private final TimeProvider timeProvider;
private final String keyId;
public LDSigningService(KeycloakSession keycloakSession, String keyId, String algorithmType, String ldpType, ObjectMapper objectMapper, TimeProvider timeProvider, Optional<String> kid) {
public LDSigningService(KeycloakSession keycloakSession, String keyId, String algorithmType, String ldpType, TimeProvider timeProvider, Optional<String> kid) {
super(keycloakSession, keyId, algorithmType);
this.timeProvider = timeProvider;
this.keyId = kid.orElse(keyId);
@ -66,7 +65,7 @@ public class LDSigningService extends SigningService<VerifiableCredential> {
linkedDataCryptographicSuite = switch (ldpType) {
case Ed255192018Suite.PROOF_TYPE ->
new Ed255192018Suite(objectMapper, signatureProvider.signer(signingKey));
new Ed255192018Suite(signatureProvider.signer(signingKey));
default -> throw new SigningServiceException(String.format("Proof Type %s is not supported.", ldpType));
};
}
@ -96,4 +95,4 @@ public class LDSigningService extends SigningService<VerifiableCredential> {
throw new SigningServiceException("Was not able to encode the signature.", e);
}
}
}
}

View file

@ -17,7 +17,6 @@
package org.keycloak.protocol.oid4vc.issuance.signing;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
@ -39,15 +38,13 @@ public class LDSigningServiceProviderFactory implements VCSigningServiceProvider
public static final String SUPPORTED_FORMAT = Format.LDP_VC;
private static final String HELP_TEXT = "Issues Verifiable Credentials in the W3C Data Model, using Linked-Data Proofs. See https://www.w3.org/TR/vc-data-model/";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public VerifiableCredentialsSigningService create(KeycloakSession session, ComponentModel model) {
String keyId = model.get(SigningProperties.KEY_ID.getKey());
String proofType = model.get(SigningProperties.PROOF_TYPE.getKey());
String algorithmType = model.get(SigningProperties.ALGORITHM_TYPE.getKey());
Optional<String> kid = Optional.ofNullable(model.get(SigningProperties.KID_HEADER.getKey()));
return new LDSigningService(session, keyId, algorithmType, proofType, OBJECT_MAPPER, new OffsetTimeProvider(), kid);
return new LDSigningService(session, keyId, algorithmType, proofType, new OffsetTimeProvider(), kid);
}
@Override
@ -58,10 +55,10 @@ public class LDSigningServiceProviderFactory implements VCSigningServiceProvider
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return VCSigningServiceProviderFactory.configurationBuilder()
.property(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
.property(SigningProperties.PROOF_TYPE.asConfigProperty())
.property(SigningProperties.KID_HEADER.asConfigProperty())
.build();
.property(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
.property(SigningProperties.PROOF_TYPE.asConfigProperty())
.property(SigningProperties.KID_HEADER.asConfigProperty())
.build();
}
@Override
@ -72,8 +69,8 @@ public class LDSigningServiceProviderFactory implements VCSigningServiceProvider
@Override
public void validateSpecificConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model)
.checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
.checkRequired(SigningProperties.PROOF_TYPE.asConfigProperty());
.checkRequired(SigningProperties.ALGORITHM_TYPE.asConfigProperty())
.checkRequired(SigningProperties.PROOF_TYPE.asConfigProperty());
}
@Override

View file

@ -32,6 +32,7 @@ import org.keycloak.sdjwt.DisclosureSpec;
import org.keycloak.sdjwt.SdJwt;
import org.keycloak.sdjwt.SdJwtUtils;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
@ -121,7 +122,7 @@ public class SdJwtSigningService extends SigningService<String> {
// Get the issuance date from the credential. Since nbf is mandatory, we set it to the current time if not
// provided
long iat = Optional.ofNullable(verifiableCredential.getIssuanceDate())
.map(issuanceDate -> issuanceDate.toInstant().getEpochSecond())
.map(Instant::getEpochSecond)
.orElse((long) timeProvider.currentTimeSeconds());
rootNode.put(NOT_BEFORE_CLAIM, iat);
if (verifiableCredential.getType() == null || verifiableCredential.getType().size() != 1) {
@ -141,4 +142,4 @@ public class SdJwtSigningService extends SigningService<String> {
return sdJwt.toSdJwtString();
}
}
}

View file

@ -31,13 +31,13 @@ import com.apicatalog.rdf.io.RdfWriter;
import com.apicatalog.rdf.io.error.RdfWriterException;
import com.apicatalog.rdf.io.error.UnsupportedContentException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.setl.rdf.normalization.RdfNormalize;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.protocol.oid4vc.issuance.signing.SigningServiceException;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.StringReader;
@ -59,13 +59,11 @@ import java.util.Optional;
*/
public class Ed255192018Suite implements LinkedDataCryptographicSuite {
private final ObjectMapper objectMapper;
private final SignatureSignerContext signerContext;
public static final String PROOF_TYPE = "Ed25519Signature2018";
public Ed255192018Suite(ObjectMapper objectMapper, SignatureSignerContext signerContext) {
this.objectMapper = objectMapper;
public Ed255192018Suite(SignatureSignerContext signerContext) {
this.signerContext = signerContext;
}
@ -79,7 +77,7 @@ public class Ed255192018Suite implements LinkedDataCryptographicSuite {
private byte[] transform(VerifiableCredential verifiableCredential) {
try {
String credentialString = objectMapper.writeValueAsString(verifiableCredential);
String credentialString = JsonSerialization.mapper.writeValueAsString(verifiableCredential);
var credentialDocument = JsonDocument.of(new StringReader(credentialString));
@ -134,4 +132,4 @@ public class Ed255192018Suite implements LinkedDataCryptographicSuite {
public String getProofType() {
return PROOF_TYPE;
}
}
}

View file

@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URI;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -50,9 +51,9 @@ public class VerifiableCredential {
private List<String> context = new ArrayList<>(List.of(VC_CONTEXT_V1));
private List<String> type = new ArrayList<>();
private URI issuer;
private Date issuanceDate;
private Instant issuanceDate;
private URI id;
private Date expirationDate;
private Instant expirationDate;
private CredentialSubject credentialSubject = new CredentialSubject();
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<>();
@ -95,11 +96,11 @@ public class VerifiableCredential {
return this;
}
public Date getIssuanceDate() {
public Instant getIssuanceDate() {
return issuanceDate;
}
public VerifiableCredential setIssuanceDate(Date issuanceDate) {
public VerifiableCredential setIssuanceDate(Instant issuanceDate) {
this.issuanceDate = issuanceDate;
return this;
}
@ -113,11 +114,11 @@ public class VerifiableCredential {
return this;
}
public Date getExpirationDate() {
public Instant getExpirationDate() {
return expirationDate;
}
public VerifiableCredential setExpirationDate(Date expirationDate) {
public VerifiableCredential setExpirationDate(Instant expirationDate) {
this.expirationDate = expirationDate;
return this;
}

View file

@ -17,7 +17,6 @@
package org.keycloak.testsuite.oid4vc.issuance.signing;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.logging.Logger;
import org.junit.Before;
import org.junit.Test;
@ -38,10 +37,10 @@ import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.util.JsonSerialization;
import java.security.PublicKey;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -126,7 +125,7 @@ public class JwtSigningServiceTest extends OID4VCTest {
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
"test", "test",
"arrayClaim", List.of("a", "b", "c"),
"issuanceDate", Date.from(Instant.ofEpochSecond(10)))));
"issuanceDate", Instant.ofEpochSecond(10))));
}
@Test
@ -184,19 +183,19 @@ public class JwtSigningServiceTest extends OID4VCTest {
try {
JsonWebToken theToken = verifier.getToken();
assertEquals("JWT claim in JWT encoded VC or VP MUST be used to set the value of the “expirationDate” of the VC", TEST_EXPIRATION_DATE.toInstant().getEpochSecond(), theToken.getExp().longValue());
assertEquals("JWT claim in JWT encoded VC or VP MUST be used to set the value of the “expirationDate” of the VC", TEST_EXPIRATION_DATE.getEpochSecond(), theToken.getExp().longValue());
if (claims.containsKey("issuanceDate")) {
assertEquals("VC Data Model v1.1 specifies that “issuanceDate” property MUST be represented as an nbf JWT claim, and not iat JWT claim.", ((Date) claims.get("issuanceDate")).toInstant().getEpochSecond(), theToken.getNbf().longValue());
assertEquals("VC Data Model v1.1 specifies that “issuanceDate” property MUST be represented as an nbf JWT claim, and not iat JWT claim.", ((Instant) claims.get("issuanceDate")).getEpochSecond(), theToken.getNbf().longValue());
} else {
// if not specific date is set, check against "currentTime"
assertEquals("VC Data Model v1.1 specifies that “issuanceDate” property MUST be represented as an nbf JWT claim, and not iat JWT claim.", TEST_ISSUANCE_DATE.toInstant().getEpochSecond(), theToken.getNbf().longValue());
assertEquals("VC Data Model v1.1 specifies that “issuanceDate” property MUST be represented as an nbf JWT claim, and not iat JWT claim.", TEST_ISSUANCE_DATE.getEpochSecond(), theToken.getNbf().longValue());
}
assertEquals("The issuer should be set in the token.", TEST_DID.toString(), theToken.getIssuer());
assertEquals("The credential ID should be set as the token ID.", testCredential.getId().toString(), theToken.getId());
Optional.ofNullable(testCredential.getCredentialSubject().getClaims().get("id")).ifPresent(id -> assertEquals("If the credentials subject id is set, it should be set as the token subject.", id.toString(), theToken.getSubject()));
assertNotNull("The credentials should be included at the vc-claim.", theToken.getOtherClaims().get("vc"));
VerifiableCredential credential = new ObjectMapper().convertValue(theToken.getOtherClaims().get("vc"), VerifiableCredential.class);
VerifiableCredential credential = JsonSerialization.mapper.convertValue(theToken.getOtherClaims().get("vc"), VerifiableCredential.class);
assertEquals("The types should be included", TEST_TYPES, credential.getType());
assertEquals("The issuer should be included", TEST_DID, credential.getIssuer());
assertEquals("The expiration date should be included", TEST_EXPIRATION_DATE, credential.getExpirationDate());
@ -213,7 +212,7 @@ public class JwtSigningServiceTest extends OID4VCTest {
}
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
if (testRealm.getComponents() != null) {
@ -223,4 +222,4 @@ public class JwtSigningServiceTest extends OID4VCTest {
Map.of("org.keycloak.keys.KeyProvider", List.of(getRsaKeyProvider(rsaKey)))));
}
}
}
}

View file

@ -17,7 +17,6 @@
package org.keycloak.testsuite.oid4vc.issuance.signing;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.crypto.CryptoIntegration;
@ -33,7 +32,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -61,7 +59,6 @@ public class LDSigningServiceTest extends OID4VCTest {
getKeyFromSession(session).getKid(),
"EdDSA",
"UnsupportedSignatureType",
new ObjectMapper(),
new StaticTimeProvider(1000),
Optional.empty()));
} catch (RunOnServerException ros) {
@ -81,7 +78,6 @@ public class LDSigningServiceTest extends OID4VCTest {
"no-such-key",
"EdDSA",
"Ed25519Signature2018",
new ObjectMapper(),
new StaticTimeProvider(1000),
Optional.empty()));
} catch (RunOnServerException ros) {
@ -113,7 +109,7 @@ public class LDSigningServiceTest extends OID4VCTest {
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
"test", "test",
"arrayClaim", List.of("a", "b", "c"),
"issuanceDate", Date.from(Instant.ofEpochSecond(10))),
"issuanceDate", Instant.ofEpochSecond(10)),
Optional.empty()));
}
@ -127,7 +123,7 @@ public class LDSigningServiceTest extends OID4VCTest {
Map.of("id", String.format("uri:uuid:%s", UUID.randomUUID()),
"test", "test",
"arrayClaim", List.of("a", "b", "c"),
"issuanceDate", Date.from(Instant.ofEpochSecond(10))),
"issuanceDate", Instant.ofEpochSecond(10)),
Optional.of("did:web:test.org#the-key-id")));
}
@ -150,7 +146,6 @@ public class LDSigningServiceTest extends OID4VCTest {
keyWrapper.getKid(),
"EdDSA",
"Ed25519Signature2018",
new ObjectMapper(),
new StaticTimeProvider(1000),
kid);

View file

@ -51,7 +51,6 @@ import org.keycloak.common.util.SecretGenerator;
import org.keycloak.crypto.Algorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory;
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
@ -89,10 +88,8 @@ import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -355,7 +352,6 @@ public class OID4VCIssuerEndpointTest extends OID4VCTest {
@Test
public void testRequestCredential() {
String token = getBearerToken(oauth);
ObjectMapper objectMapper = new ObjectMapper();
testingClient
.server(TEST_REALM_NAME)
.run((session -> {
@ -373,7 +369,9 @@ public class OID4VCIssuerEndpointTest extends OID4VCTest {
assertNotNull("A valid credential string should have been responded", jsonWebToken);
assertNotNull("The credentials should be included at the vc-claim.", jsonWebToken.getOtherClaims().get("vc"));
VerifiableCredential credential = objectMapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
VerifiableCredential credential = //
JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get(
"vc"), VerifiableCredential.class);
assertNotNull("@context is a required VC property", credential.getContext());
assertEquals(1, credential.getContext().size());
assertEquals(VerifiableCredential.VC_CONTEXT_V1, credential.getContext().get(0));
@ -613,7 +611,7 @@ public class OID4VCIssuerEndpointTest extends OID4VCTest {
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken();
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
VerifiableCredential credential = new ObjectMapper().convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
assertEquals(TEST_TYPES, credential.getType());
assertEquals(TEST_DID, credential.getIssuer());
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));
@ -704,7 +702,7 @@ public class OID4VCIssuerEndpointTest extends OID4VCTest {
assertNotNull("The credential should have been responded.", credentialResponse.getCredential());
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken();
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
VerifiableCredential credential = new ObjectMapper().convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
assertEquals(List.of("VerifiableCredential"), credential.getType());
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));

View file

@ -52,7 +52,6 @@ import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -67,8 +66,8 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
protected static final String CONTEXT_URL = "https://www.w3.org/2018/credentials/v1";
protected static final URI TEST_DID = URI.create("did:web:test.org");
protected static final List<String> TEST_TYPES = List.of("VerifiableCredential");
protected static final Date TEST_EXPIRATION_DATE = Date.from(Instant.ofEpochSecond(2000));
protected static final Date TEST_ISSUANCE_DATE = Date.from(Instant.ofEpochSecond(1000));
protected static final Instant TEST_EXPIRATION_DATE = Instant.ofEpochSecond(2000);
protected static final Instant TEST_ISSUANCE_DATE = Instant.ofEpochSecond(1000);
protected static final KeyWrapper RSA_KEY = getRsaKey();
@ -87,7 +86,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
testCredential.setIssuer(TEST_DID);
testCredential.setExpirationDate(TEST_EXPIRATION_DATE);
if (claims.containsKey("issuanceDate")) {
testCredential.setIssuanceDate((Date) claims.get("issuanceDate"));
testCredential.setIssuanceDate((Instant) claims.get("issuanceDate"));
}
testCredential.setCredentialSubject(getCredentialSubject(claims));

View file

@ -243,7 +243,7 @@ public class SdJwtSigningServiceTest extends OID4VCTest {
assertEquals("The credential ID should be set as the token ID.", testCredential.getId().toString(), theToken.getId());
assertEquals("The type should be included", TEST_TYPES.get(0), theToken.getOtherClaims().get("vct"));
assertEquals("The nbf date should be included", TEST_ISSUANCE_DATE.toInstant().getEpochSecond(), theToken.getNbf().longValue());
assertEquals("The nbf date should be included", TEST_ISSUANCE_DATE.getEpochSecond(), theToken.getNbf().longValue());
List<String> sds = (List<String>) theToken.getOtherClaims().get("_sd");
if (sds != null && !sds.isEmpty()){