From 1f94cfff62c888349a9f400f2e8e9ec97b3076a2 Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Fri, 15 Sep 2023 11:02:51 -0400 Subject: [PATCH] Update signing result for leaf only certs create bundles with only leaf certificates hand verification of certs in both situations - only untrusted certs (current behavior) - full cert chain to fulcio root (legacy) also move SigningCert object, it was a pointless wrapper around CertPath Signed-off-by: Appu Goundan --- .../java/fuzzing/FulcioVerifierFuzzer.java | 28 +-- .../java/dev/sigstore/KeylessSignature.java | 6 +- .../main/java/dev/sigstore/KeylessSigner.java | 14 +- .../java/dev/sigstore/KeylessVerifier.java | 5 +- .../encryption/certificates/Certificates.java | 10 +- .../sigstore/fulcio/client/FulcioClient.java | 33 ++- .../fulcio/client/FulcioVerifier.java | 57 ++--- .../fulcio/client/SigningCertificate.java | 197 ------------------ .../{Keyless2Test.java => KeylessTest.java} | 21 +- .../certificates/CertificatesTest.java | 6 +- .../fulcio/client/FulcioClientTest.java | 53 +++-- .../fulcio/client/FulcioVerifierTest.java | 46 +--- .../fulcio/client/SigningCertificateTest.java | 112 ---------- .../fulcio-response/valid/fulcio.crt.pem | 14 ++ .../samples/fulcio-response/valid/sct.base64 | 1 - 15 files changed, 164 insertions(+), 439 deletions(-) delete mode 100644 sigstore-java/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java rename sigstore-java/src/test/java/dev/sigstore/{Keyless2Test.java => KeylessTest.java} (89%) delete mode 100644 sigstore-java/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java delete mode 100644 sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/sct.base64 diff --git a/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java b/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java index acc67a50..b332867c 100644 --- a/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java +++ b/fuzzing/src/main/java/fuzzing/FulcioVerifierFuzzer.java @@ -18,11 +18,11 @@ import com.code_intelligence.jazzer.api.FuzzedDataProvider; import dev.sigstore.fulcio.client.FulcioVerificationException; import dev.sigstore.fulcio.client.FulcioVerifier; -import dev.sigstore.fulcio.client.SigningCertificate; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPath; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -34,33 +34,19 @@ public class FulcioVerifierFuzzer { public static void fuzzerTestOneInput(FuzzedDataProvider data) { try { - int[] intArray = data.consumeInts(data.consumeInt(1, 10)); - var cas = Tuf.certificateAuthoritiesFrom(data); var ctLogs = Tuf.transparencyLogsFrom(data); - byte[] byteArray = data.consumeRemainingAsBytes(); - List certList = new ArrayList(); + List certList = new ArrayList<>(); CertificateFactory cf = CertificateFactory.getInstance("X.509"); - certList.add(cf.generateCertificate(new ByteArrayInputStream(byteArray))); - certList.add(cf.generateCertificate(new ByteArrayInputStream(byteArray))); + certList.add(cf.generateCertificate(new ByteArrayInputStream(data.consumeBytes(10240)))); + certList.add( + cf.generateCertificate(new ByteArrayInputStream(data.consumeRemainingAsBytes()))); - SigningCertificate sc = SigningCertificate.from(cf.generateCertPath(certList)); + CertPath sc = cf.generateCertPath(certList); FulcioVerifier fv = FulcioVerifier.newFulcioVerifier(cas, ctLogs); - for (int choice : intArray) { - switch (choice % 4) { - case 0: - sc.getCertificates(); - break; - case 1: - sc.getLeafCertificate(); - break; - case 3: - fv.verifySigningCertificate(sc); - break; - } - } + fv.verifySigningCertificate(sc); } catch (CertificateException | FulcioVerificationException | InvalidKeySpecException diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSignature.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSignature.java index b2f60f53..b3ad5483 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSignature.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSignature.java @@ -25,7 +25,11 @@ public interface KeylessSignature { /** The sha256 hash digest of the artifact */ byte[] getDigest(); - /** The full certificate chain provided by fulcio for the public key used to sign the artifact */ + /** + * The partial certificate chain provided by fulcio for the public key and identity used to sign + * the artifact, this should NOT contain the trusted root or any trusted intermediates. But users + * of this object should understand that older signatures may include the full chain. + */ CertPath getCertPath(); /** The signature over the artifact */ diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java index dec8673e..e7d7e2c2 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java @@ -29,7 +29,6 @@ import dev.sigstore.fulcio.client.FulcioClient; import dev.sigstore.fulcio.client.FulcioVerificationException; import dev.sigstore.fulcio.client.FulcioVerifier; -import dev.sigstore.fulcio.client.SigningCertificate; import dev.sigstore.fulcio.client.UnsupportedAlgorithmException; import dev.sigstore.oidc.client.OidcClients; import dev.sigstore.oidc.client.OidcException; @@ -47,6 +46,7 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; +import java.security.cert.CertPath; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; import java.time.Duration; @@ -82,7 +82,7 @@ public class KeylessSigner implements AutoCloseable { /** The code signing certificate from Fulcio. */ @GuardedBy("lock") - private @Nullable SigningCertificate signingCert; + private @Nullable CertPath signingCert; /** * Representation {@link #signingCert} in PEM bytes format. This is used to avoid serializing the @@ -244,7 +244,7 @@ public List sign(List artifactDigests) // However, files might be large, and it might take time to talk to Rekor // so we check the certificate expiration here. renewSigningCertificate(); - SigningCertificate signingCert; + CertPath signingCert; byte[] signingCertPemBytes; lock.readLock().lock(); try { @@ -266,7 +266,7 @@ public List sign(List artifactDigests) result.add( KeylessSignature.builder() .digest(artifactDigest) - .certPath(signingCert.getCertPath()) + .certPath(signingCert) .signature(signature) .entry(rekorResponse.getEntry()) .build()); @@ -284,7 +284,7 @@ private void renewSigningCertificate() if (signingCert != null) { @SuppressWarnings("JavaUtilDate") long lifetimeLeft = - signingCert.getLeafCertificate().getNotAfter().getTime() - System.currentTimeMillis(); + Certificates.getLeaf(signingCert).getNotAfter().getTime() - System.currentTimeMillis(); if (lifetimeLeft > minSigningCertificateLifetime.toMillis()) { // The current certificate is fine, reuse it return; @@ -300,7 +300,7 @@ private void renewSigningCertificate() signingCert = null; signingCertPemBytes = null; OidcToken tokenInfo = oidcClients.getIDToken(); - SigningCertificate signingCert = + CertPath signingCert = fulcioClient.signingCertificate( CertificateRequest.newCertificateRequest( signer.getPublicKey(), @@ -311,7 +311,7 @@ private void renewSigningCertificate() // allow that to be known fulcioVerifier.verifySigningCertificate(signingCert); this.signingCert = signingCert; - signingCertPemBytes = Certificates.toPemBytes(signingCert.getLeafCertificate()); + signingCertPemBytes = Certificates.toPemBytes(signingCert); } finally { lock.writeLock().unlock(); } diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java index 2a380e5a..77d0a7ec 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessVerifier.java @@ -24,7 +24,6 @@ import dev.sigstore.fulcio.client.FulcioCertificateVerifier; import dev.sigstore.fulcio.client.FulcioVerificationException; import dev.sigstore.fulcio.client.FulcioVerifier; -import dev.sigstore.fulcio.client.SigningCertificate; import dev.sigstore.rekor.client.HashedRekordRequest; import dev.sigstore.rekor.client.RekorClient; import dev.sigstore.rekor.client.RekorEntry; @@ -142,8 +141,8 @@ public void verify(Path artifact, KeylessVerificationRequest request) */ public void verify(byte[] artifactDigest, KeylessVerificationRequest request) throws KeylessVerificationException { - var signingCert = SigningCertificate.from(request.getKeylessSignature().getCertPath()); - var leafCert = signingCert.getLeafCertificate(); + var signingCert = request.getKeylessSignature().getCertPath(); + var leafCert = Certificates.getLeaf(signingCert); // this ensures the provided artifact digest matches what may have come from a bundle (in // keyless signature) diff --git a/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java b/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java index 05ba6eef..6688df6f 100644 --- a/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java +++ b/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java @@ -140,12 +140,14 @@ public static CertPath toCertPath(Certificate certificate) throws CertificateExc return cf.generateCertPath(Collections.singletonList(certificate)); } - /** Appends a CertPath to another {@link CertPath} as children. */ - public static CertPath appendCertPath(CertPath parent, Certificate child) - throws CertificateException { + /** Appends an CertPath to another {@link CertPath} as children. */ + public static CertPath append(CertPath parent, CertPath child) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); List certs = - ImmutableList.builder().add(child).addAll(parent.getCertificates()).build(); + ImmutableList.builder() + .addAll(child.getCertificates()) + .addAll(parent.getCertificates()) + .build(); return cf.generateCertPath(certs); } diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java index ba878434..db74f6f3 100644 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioClient.java @@ -17,8 +17,11 @@ import static dev.sigstore.fulcio.v2.SigningCertificate.CertificateCase.SIGNED_CERTIFICATE_DETACHED_SCT; +import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.ByteString; +import dev.sigstore.encryption.certificates.Certificates; import dev.sigstore.fulcio.v2.CAGrpc; +import dev.sigstore.fulcio.v2.CertificateChain; import dev.sigstore.fulcio.v2.CreateSigningCertificateRequest; import dev.sigstore.fulcio.v2.Credentials; import dev.sigstore.fulcio.v2.PublicKey; @@ -28,7 +31,13 @@ import dev.sigstore.http.ImmutableHttpParams; import dev.sigstore.trustroot.CertificateAuthority; import dev.sigstore.trustroot.SigstoreTrustedRoot; +import java.io.ByteArrayInputStream; +import java.security.cert.CertPath; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Base64; import java.util.concurrent.TimeUnit; @@ -80,9 +89,9 @@ public FulcioClient build() { * Request a signing certificate from fulcio. * * @param request certificate request parameters - * @return a {@link SigningCertificate} from fulcio + * @return a {@link CertPath} from fulcio */ - public SigningCertificate signingCertificate(CertificateRequest request) + public CertPath signingCertificate(CertificateRequest request) throws InterruptedException, CertificateException { if (!certificateAuthority.isCurrent()) { throw new RuntimeException( @@ -126,9 +135,27 @@ public SigningCertificate signingCertificate(CertificateRequest request) if (certs.getCertificateCase() == SIGNED_CERTIFICATE_DETACHED_SCT) { throw new CertificateException("Detached SCTs are not supported"); } - return SigningCertificate.newSigningCertificate(certs.getSignedCertificateEmbeddedSct()); + return Certificates.trimParent( + decodeCerts(certs.getSignedCertificateEmbeddedSct().getChain()), + certificateAuthority.getCertPath()); } finally { channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } } + + @VisibleForTesting + CertPath decodeCerts(CertificateChain certChain) throws CertificateException { + var certificateFactory = CertificateFactory.getInstance("X.509"); + var certs = new ArrayList(); + if (certChain.getCertificatesCount() == 0) { + throw new CertificateParsingException( + "no valid PEM certificates were found in response from Fulcio"); + } + for (var cert : certChain.getCertificatesList().asByteStringList()) { + certs.add( + (X509Certificate) + certificateFactory.generateCertificate(new ByteArrayInputStream(cert.toByteArray()))); + } + return certificateFactory.generateCertPath(certs); + } } diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java index c708b7c1..4fbdac72 100644 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java +++ b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/FulcioVerifier.java @@ -44,7 +44,7 @@ import java.util.Map; import java.util.stream.Collectors; -/** Verifier for fulcio {@link SigningCertificate}. */ +/** Verifier for fulcio generated signing cerificates */ public class FulcioVerifier { private final CertificateAuthorities cas; private final TransparencyLogs ctLogs; @@ -90,25 +90,21 @@ private FulcioVerifier( } @VisibleForTesting - void verifySct(SigningCertificate signingCertificate, CertPath rebuiltCert) - throws FulcioVerificationException { + void verifySct(CertPath fullCertPath) throws FulcioVerificationException { if (ctLogs.size() == 0) { throw new FulcioVerificationException("No ct logs were provided to verifier"); } - if (signingCertificate.getDetachedSct().isPresent()) { - throw new FulcioVerificationException( - "Detached SCTs are not supported for validating certificates"); - } else if (signingCertificate.getEmbeddedSct().isPresent()) { - verifyEmbeddedScts(rebuiltCert); + if (Certificates.getEmbeddedSCTs(Certificates.getLeaf(fullCertPath)).isPresent()) { + verifyEmbeddedScts(fullCertPath); } else { throw new FulcioVerificationException("No valid SCTs were found during verification"); } } - private void verifyEmbeddedScts(CertPath rebuiltCert) throws FulcioVerificationException { + private void verifyEmbeddedScts(CertPath certPath) throws FulcioVerificationException { @SuppressWarnings("unchecked") - var certs = (List) rebuiltCert.getCertificates(); + var certs = (List) certPath.getCertificates(); CTVerificationResult result; try { result = ctVerifier.verifySignedCertificateTimestamps(certs, null, null); @@ -144,22 +140,23 @@ private void verifyEmbeddedScts(CertPath rebuiltCert) throws FulcioVerificationE * configured in this validator. Also verify that the leaf certificate contains at least one valid * SCT * - * @param signingCertificate containing the certificate chain + * @param signingCertificate containing a certificate chain, this chain should not contain any + * trusted root or trusted intermediates * @throws FulcioVerificationException if verification fails for any reason */ - public void verifySigningCertificate(SigningCertificate signingCertificate) + public void verifySigningCertificate(CertPath signingCertificate) throws FulcioVerificationException, IOException { - CertPath reconstructedCert = reconstructValidCertPath(signingCertificate); - verifySct(signingCertificate, reconstructedCert); + CertPath fullCertPath = validateCertPath(signingCertificate); + verifySct(fullCertPath); } /** * Find a valid cert path that chains back up to the trusted root certs and reconstruct a * certificate path combining the provided un-trusted certs and a known set of trusted and - * intermediate certs. + * intermediate certs. If a full certificate is provided with a self signed root, this should + * attempt to match the root/intermediate with a trusted chain. */ - CertPath reconstructValidCertPath(SigningCertificate signingCertificate) - throws FulcioVerificationException { + CertPath validateCertPath(CertPath signingCertificate) throws FulcioVerificationException { CertPathValidator cpv; try { cpv = CertPathValidator.getInstance("PKIX"); @@ -170,7 +167,7 @@ CertPath reconstructValidCertPath(SigningCertificate signingCertificate) e); } - var leaf = signingCertificate.getLeafCertificate(); + var leaf = Certificates.getLeaf(signingCertificate); var validCAs = cas.find(leaf.getNotBefore().toInstant()); if (validCAs.size() == 0) { @@ -194,17 +191,27 @@ CertPath reconstructValidCertPath(SigningCertificate signingCertificate) // these certs are only valid for 15 minutes, so find a time in the validity period @SuppressWarnings("JavaUtilDate") Date dateInValidityPeriod = - new Date(signingCertificate.getLeafCertificate().getNotBefore().getTime()); + new Date(Certificates.getLeaf(signingCertificate).getNotBefore().getTime()); pkixParams.setDate(dateInValidityPeriod); - CertPath rebuiltCert; + CertPath fullCertPath; try { - // build a cert chain with the root-chain in question and the provided leaf - rebuiltCert = - Certificates.appendCertPath(ca.getCertPath(), signingCertificate.getLeafCertificate()); + if (Certificates.isSelfSigned(signingCertificate)) { + if (Certificates.containsParent(signingCertificate, ca.getCertPath())) { + fullCertPath = signingCertificate; + } else { + // verification failed because we didn't match to a trusted root + caVerificationFailure.put( + ca.getUri().toString(), "Trusted root in chain does not match"); + continue; + } + } else { + // build a cert chain with the root-chain in question and the provided signing certificate + fullCertPath = Certificates.append(ca.getCertPath(), signingCertificate); + } // a result is returned here, but we ignore it - cpv.validate(rebuiltCert, pkixParams); + cpv.validate(fullCertPath, pkixParams); } catch (CertPathValidatorException | InvalidAlgorithmParameterException | CertificateException ve) { @@ -212,7 +219,7 @@ CertPath reconstructValidCertPath(SigningCertificate signingCertificate) // verification failed continue; } - return rebuiltCert; + return fullCertPath; // verification passed so just end this method } String errors = diff --git a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java b/sigstore-java/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java deleted file mode 100644 index 42626183..00000000 --- a/sigstore-java/src/main/java/dev/sigstore/fulcio/client/SigningCertificate.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2022 The Sigstore Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.sigstore.fulcio.client; - -import static dev.sigstore.json.GsonSupplier.GSON; - -import com.google.api.client.util.PemReader; -import com.google.common.annotations.VisibleForTesting; -import com.google.gson.JsonParseException; -import dev.sigstore.encryption.certificates.transparency.DigitallySigned; -import dev.sigstore.encryption.certificates.transparency.SerializationException; -import dev.sigstore.encryption.certificates.transparency.SignedCertificateTimestamp; -import dev.sigstore.fulcio.v2.CertificateChain; -import dev.sigstore.fulcio.v2.SigningCertificateDetachedSCT; -import dev.sigstore.fulcio.v2.SigningCertificateEmbeddedSCT; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.StringReader; -import java.nio.charset.StandardCharsets; -import java.security.cert.*; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.Optional; -import javax.annotation.Nullable; - -/** - * Response from Fulcio that includes a Certificate Chain and a Signed Certificate Timestamp (SCT). - * - *

An SCT is not required for all instances of fulcio, however the public good instance of fulcio - * should probably always include one. SCT can be associated with a certificate in two modes: - * - *

In detached mode -- fulcio provides the SCT via a fulcio specific non-standard header in the - * response as base64 encoded json. - * - *

In embedded mode -- fulcio generates certificates with the SCT embedded as an extension in the - * x509 certificates (this is the most common form, and we should expect this moving forward) - */ -public class SigningCertificate { - private static final String SCT_X509_OID = "1.3.6.1.4.1.11129.2.4.2"; - - private final CertPath certPath; - @Nullable private final SignedCertificateTimestamp sct; - - public static SigningCertificate from(CertPath certPath) { - return new SigningCertificate(certPath); - } - - static SigningCertificate newSigningCertificate(String certs, @Nullable String sctHeader) - throws CertificateException, IOException, SerializationException { - CertPath certPath = decodeCerts(certs); - if (sctHeader != null) { - SignedCertificateTimestamp sct = - decodeSCT(new String(Base64.getDecoder().decode(sctHeader), StandardCharsets.UTF_8)); - return new SigningCertificate(certPath, sct); - } - return new SigningCertificate(certPath, null); - } - - static SigningCertificate newSigningCertificate(SigningCertificateDetachedSCT signingCertificate) - throws CertificateException, SerializationException { - SignedCertificateTimestamp sct = null; - if (!signingCertificate.getSignedCertificateTimestamp().isEmpty()) { - sct = decodeSCT(signingCertificate.getSignedCertificateTimestamp().toStringUtf8()); - } - return new SigningCertificate(decodeCerts(signingCertificate.getChain()), sct); - } - - static SigningCertificate newSigningCertificate(SigningCertificateEmbeddedSCT signingCertificate) - throws CertificateException { - return new SigningCertificate(decodeCerts(signingCertificate.getChain())); - } - - @VisibleForTesting - static CertPath decodeCerts(CertificateChain certChain) throws CertificateException { - var certificateFactory = CertificateFactory.getInstance("X.509"); - var certs = new ArrayList(); - if (certChain.getCertificatesCount() == 0) { - throw new CertificateParsingException( - "no valid PEM certificates were found in response from Fulcio"); - } - for (var cert : certChain.getCertificatesList().asByteStringList()) { - certs.add( - (X509Certificate) - certificateFactory.generateCertificate(new ByteArrayInputStream(cert.toByteArray()))); - } - return certificateFactory.generateCertPath(certs); - } - - @VisibleForTesting - static CertPath decodeCerts(String content) throws CertificateException, IOException { - PemReader pemReader = new PemReader(new StringReader(content)); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - ArrayList certList = new ArrayList<>(); - while (true) { - PemReader.Section section = pemReader.readNextSection(); - if (section == null) { - break; - } - - byte[] certBytes = section.getBase64DecodedBytes(); - certList.add((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes))); - } - if (certList.isEmpty()) { - throw new CertificateParsingException( - "no valid PEM certificates were found in response from Fulcio"); - } - return cf.generateCertPath(certList); - } - - @VisibleForTesting - static SignedCertificateTimestamp decodeSCT(String sctJson) throws SerializationException { - return GSON.get().fromJson(sctJson, SctJson.class).toSct(); - } - - /** Returns true if the signing certificate contains scts embedded in X509 extensions. */ - boolean hasEmbeddedSct() { - return getLeafCertificate().getExtensionValue(SCT_X509_OID) != null; - } - - /** - * Returns scts if present, or empty if not. The returned byte array may contain any number of - * embedded scts. - */ - Optional getEmbeddedSct() { - return Optional.ofNullable(getLeafCertificate().getExtensionValue(SCT_X509_OID)); - } - - private static class SctJson { - private int sct_version; - private byte[] id; - private long timestamp; - private byte[] extensions; - private byte[] signature; - - public SignedCertificateTimestamp toSct() throws JsonParseException, SerializationException { - if (sct_version != 0) { - throw new JsonParseException( - "Invalid SCT version:" + sct_version + ", only 0 (V1) is allowed"); - } - if (extensions.length != 0) { - throw new JsonParseException( - "SCT has extensions that cannot be handled by client:" - + new String(extensions, StandardCharsets.UTF_8)); - } - - DigitallySigned digiSig = DigitallySigned.decode(signature); - return new SignedCertificateTimestamp( - SignedCertificateTimestamp.Version.V1, - id, - timestamp, - extensions, - digiSig, - SignedCertificateTimestamp.Origin.OCSP_RESPONSE); - } - } - - private SigningCertificate(CertPath certPath, SignedCertificateTimestamp sct) { - this.certPath = certPath; - this.sct = sct; - } - - private SigningCertificate(CertPath certPath) { - this.certPath = certPath; - this.sct = null; - } - - public CertPath getCertPath() { - return certPath; - } - - @SuppressWarnings("unchecked") - public List getCertificates() { - return (List) certPath.getCertificates(); - } - - public X509Certificate getLeafCertificate() { - return (X509Certificate) certPath.getCertificates().get(0); - } - - Optional getDetachedSct() { - return Optional.ofNullable(sct); - } -} diff --git a/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java b/sigstore-java/src/test/java/dev/sigstore/KeylessTest.java similarity index 89% rename from sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java rename to sigstore-java/src/test/java/dev/sigstore/KeylessTest.java index a7af7ff6..5362194e 100644 --- a/sigstore-java/src/test/java/dev/sigstore/Keyless2Test.java +++ b/sigstore-java/src/test/java/dev/sigstore/KeylessTest.java @@ -36,7 +36,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -public class Keyless2Test { +public class KeylessTest { @TempDir public static Path testRoot; public static List artifactDigests; @@ -68,10 +68,12 @@ public void sign_production() throws Exception { verifySigningResult(results); var verifier = KeylessVerifier.builder().sigstorePublicDefaults().build(); - for (var result : results) { - verifier.verifyOnline( - result.getDigest(), Certificates.toPemBytes(result.getCertPath()), result.getSignature()); - checkBundleSerialization(result); + + for (int i = 0; i < results.size(); i++) { + verifier.verify( + artifactDigests.get(i), + KeylessVerificationRequest.builder().keylessSignature(results.get(i)).build()); + checkBundleSerialization(results.get(i)); } } @@ -84,10 +86,11 @@ public void sign_staging() throws Exception { verifySigningResult(results); var verifier = KeylessVerifier.builder().sigstoreStagingDefaults().build(); - for (var result : results) { - verifier.verifyOnline( - result.getDigest(), Certificates.toPemBytes(result.getCertPath()), result.getSignature()); - checkBundleSerialization(result); + for (int i = 0; i < results.size(); i++) { + verifier.verify( + artifactDigests.get(i), + KeylessVerificationRequest.builder().keylessSignature(results.get(i)).build()); + checkBundleSerialization(results.get(i)); } } diff --git a/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java b/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java index 6d70d0b7..7189fb61 100644 --- a/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/encryption/certificates/CertificatesTest.java @@ -137,13 +137,13 @@ public void toCertPath() throws Exception { public void appendCertPath() throws Exception { var parent = Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT_CHAIN))); - var child = Certificates.fromPem(Resources.toByteArray(Resources.getResource(CERT_GH))); + var child = Certificates.fromPemChain(Resources.toByteArray(Resources.getResource(CERT_GH))); Assertions.assertEquals(2, parent.getCertificates().size()); - var appended = Certificates.appendCertPath(parent, child); + var appended = Certificates.append(parent, child); Assertions.assertEquals(3, appended.getCertificates().size()); - Assertions.assertEquals(child, appended.getCertificates().get(0)); + Assertions.assertEquals(child.getCertificates().get(0), appended.getCertificates().get(0)); Assertions.assertEquals(parent.getCertificates().get(0), appended.getCertificates().get(1)); Assertions.assertEquals(parent.getCertificates().get(1), appended.getCertificates().get(2)); } diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java index 12855f5a..c2a39cde 100644 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioClientTest.java @@ -15,11 +15,14 @@ */ package dev.sigstore.fulcio.client; +import com.google.common.io.Resources; +import dev.sigstore.encryption.certificates.Certificates; import dev.sigstore.encryption.signers.Signers; import dev.sigstore.http.ImmutableHttpParams; import dev.sigstore.testing.FakeCTLogServer; import dev.sigstore.testing.FulcioWrapper; import dev.sigstore.testing.MockOAuth2ServerExtension; +import dev.sigstore.testing.grpc.GrpcTypes; import dev.sigstore.trustroot.CertificateAuthority; import dev.sigstore.trustroot.ImmutableCertificateAuthority; import dev.sigstore.trustroot.ImmutableValidFor; @@ -41,12 +44,6 @@ public class FulcioClientTest { public void testSigningCert( MockOAuth2ServerExtension mockOAuthServerExtension, FulcioWrapper fulcioWrapper) throws Exception { - var c = - FulcioClient.builder() - .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) - .setCertificateAuthority(createCA(fulcioWrapper.getGrpcURI2())) - .build(); - // create a "subject" and sign it with the oidc server key (signed JWT) var token = mockOAuthServerExtension.getOidcToken().getIdToken(); var subject = mockOAuthServerExtension.getOidcToken().getSubjectAlternativeName(); @@ -58,11 +55,18 @@ public void testSigningCert( var cReq = CertificateRequest.newCertificateRequest(signer.getPublicKey(), token, signed); // ask fulcio for a signing cert - var sc = c.signingCertificate(cReq); + var client = + FulcioClient.builder() + .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) + .setCertificateAuthority( + createCA(fulcioWrapper.getGrpcURI2(), fulcioWrapper.getTrustBundle())) + .build(); + + var sc = client.signingCertificate(cReq); // some pretty basic assertions - Assertions.assertTrue(sc.getCertPath().getCertificates().size() > 0); - Assertions.assertTrue(sc.hasEmbeddedSct()); + Assertions.assertTrue(sc.getCertificates().size() > 0); + Assertions.assertTrue(Certificates.getEmbeddedSCTs(Certificates.getLeaf(sc)).isPresent()); } @Test @@ -70,11 +74,6 @@ public void testSigningCert( public void testSigningCert_NoSct( MockOAuth2ServerExtension mockOAuthServerExtension, FulcioWrapper fulcioWrapper) throws Exception { - var c = - FulcioClient.builder() - .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) - .setCertificateAuthority(createCA(fulcioWrapper.getGrpcURI2())) - .build(); // create a "subject" and sign it with the oidc server key (signed JWT) var token = mockOAuthServerExtension.getOidcToken().getIdToken(); @@ -87,16 +86,36 @@ public void testSigningCert_NoSct( var cReq = CertificateRequest.newCertificateRequest(signer.getPublicKey(), token, signed); // ask fulcio for a signing cert - var ex = Assertions.assertThrows(CertificateException.class, () -> c.signingCertificate(cReq)); + var client = + FulcioClient.builder() + .setHttpParams(ImmutableHttpParams.builder().allowInsecureConnections(true).build()) + .setCertificateAuthority( + createCA(fulcioWrapper.getGrpcURI2(), fulcioWrapper.getTrustBundle())) + .build(); + var ex = + Assertions.assertThrows(CertificateException.class, () -> client.signingCertificate(cReq)); Assertions.assertEquals(ex.getMessage(), "Detached SCTs are not supported"); } - private CertificateAuthority createCA(URI uri) { + private CertificateAuthority createCA(URI uri, CertPath trustBundle) { return ImmutableCertificateAuthority.builder() .uri(uri) - .certPath(Mockito.mock(CertPath.class)) + .certPath(trustBundle) .subject(Mockito.mock(Subject.class)) .validFor(ImmutableValidFor.builder().start(Instant.EPOCH).build()) .build(); } + + @Test + public void testDecode_embeddedGrpc() throws Exception { + var certs = + GrpcTypes.PemToCertificateChain( + Resources.toString( + Resources.getResource("dev/sigstore/samples/fulcio-response/valid/certWithSct.pem"), + StandardCharsets.UTF_8)); + var signingCert = FulcioClient.builder().build().decodeCerts(certs); + Assertions.assertTrue( + Certificates.getEmbeddedSCTs(Certificates.getLeaf(signingCert)).isPresent()); + Assertions.assertEquals(3, signingCert.getCertificates().size()); + } } diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java index 9dd0429b..8051b436 100644 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/FulcioVerifierTest.java @@ -18,7 +18,7 @@ import com.google.common.io.Resources; import com.google.protobuf.util.JsonFormat; import dev.sigstore.bundle.BundleFactory; -import dev.sigstore.encryption.certificates.transparency.SerializationException; +import dev.sigstore.encryption.certificates.Certificates; import dev.sigstore.proto.trustroot.v1.TrustedRoot; import dev.sigstore.trustroot.ImmutableLogId; import dev.sigstore.trustroot.ImmutableTransparencyLog; @@ -27,17 +27,12 @@ import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.spec.InvalidKeySpecException; import java.util.Collections; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class FulcioVerifierTest { - private static String sctBase64; private static String certs; private static String certsWithEmbeddedSct; private static String bundleFile; @@ -46,10 +41,6 @@ public class FulcioVerifierTest { @BeforeAll public static void loadResources() throws IOException { - sctBase64 = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/sct.base64"), - StandardCharsets.UTF_8); certs = Resources.toString( Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), @@ -67,7 +58,7 @@ public static void loadResources() throws IOException { } @BeforeAll - public static void initTrustRoot() throws IOException, CertificateException { + public static void initTrustRoot() throws Exception { var json = Resources.toString( Resources.getResource("dev/sigstore/trustroot/trusted_root.json"), @@ -79,24 +70,8 @@ public static void initTrustRoot() throws IOException, CertificateException { } @Test - public void detachedSctNotSupported() throws Exception { - var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); - - var signingCertificate = SigningCertificate.newSigningCertificate(certs, sctBase64); - var ex = - Assertions.assertThrows( - FulcioVerificationException.class, - () -> fulcioVerifier.verifySct(signingCertificate, signingCertificate.getCertPath())); - Assertions.assertEquals( - "Detached SCTs are not supported for validating certificates", ex.getMessage()); - } - - @Test - public void testVerifySct_nullCtLogKey() - throws IOException, SerializationException, CertificateException, InvalidKeySpecException, - NoSuchAlgorithmException, InvalidAlgorithmParameterException { - var signingCertificate = - SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, sctBase64); + public void testVerifySct_nullCtLogKey() throws Exception { + var signingCertificate = Certificates.fromPemChain(certsWithEmbeddedSct); var fulcioVerifier = FulcioVerifier.newFulcioVerifier( trustRoot.getCAs(), @@ -114,11 +89,11 @@ public void testVerifySct_nullCtLogKey() @Test public void testVerifySct_noSct() throws Exception { - var signingCertificate = SigningCertificate.newSigningCertificate(certs, null); + var signingCertificate = Certificates.fromPemChain(certs); var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); try { - fulcioVerifier.verifySct(signingCertificate, signingCertificate.getCertPath()); + fulcioVerifier.verifySct(signingCertificate); Assertions.fail(); } catch (FulcioVerificationException fve) { Assertions.assertEquals("No valid SCTs were found during verification", fve.getMessage()); @@ -127,7 +102,7 @@ public void testVerifySct_noSct() throws Exception { @Test public void validSigningCertAndEmbeddedSct() throws Exception { - var signingCertificate = SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, null); + var signingCertificate = Certificates.fromPemChain(certsWithEmbeddedSct); var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); fulcioVerifier.verifySigningCertificate(signingCertificate); @@ -139,12 +114,12 @@ public void validBundle() throws Exception { var fulcioVerifier = FulcioVerifier.newFulcioVerifier(trustRoot); Assertions.assertEquals(1, bundle.getCertPath().getCertificates().size()); - fulcioVerifier.verifySigningCertificate(SigningCertificate.from(bundle.getCertPath())); + fulcioVerifier.verifySigningCertificate(bundle.getCertPath()); } @Test public void invalidEmbeddedSct() throws Exception { - var signingCertificate = SigningCertificate.newSigningCertificate(certsWithEmbeddedSct, null); + var signingCertificate = Certificates.fromPemChain(certsWithEmbeddedSct); var fulcioVerifier = FulcioVerifier.newFulcioVerifier( trustRoot.getCAs(), @@ -161,8 +136,7 @@ public void invalidEmbeddedSct() throws Exception { var fve = Assertions.assertThrows( - FulcioVerificationException.class, - () -> fulcioVerifier.verifySct(signingCertificate, signingCertificate.getCertPath())); + FulcioVerificationException.class, () -> fulcioVerifier.verifySct(signingCertificate)); Assertions.assertEquals("No valid SCTs were found, all(1) SCTs were invalid", fve.getMessage()); } } diff --git a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java b/sigstore-java/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java deleted file mode 100644 index 29d8a9e9..00000000 --- a/sigstore-java/src/test/java/dev/sigstore/fulcio/client/SigningCertificateTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2022 The Sigstore Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.sigstore.fulcio.client; - -import com.google.common.io.Resources; -import com.google.protobuf.ByteString; -import dev.sigstore.encryption.certificates.transparency.SerializationException; -import dev.sigstore.fulcio.v2.SigningCertificateDetachedSCT; -import dev.sigstore.fulcio.v2.SigningCertificateEmbeddedSCT; -import dev.sigstore.testing.grpc.GrpcTypes; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateParsingException; -import java.util.Base64; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class SigningCertificateTest { - @Test - public void testDecode() throws SerializationException, IOException, CertificateException { - String sctBase64 = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/sct.base64"), - StandardCharsets.UTF_8); - String certs = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), - StandardCharsets.UTF_8); - - var signingCert = SigningCertificate.newSigningCertificate(certs, sctBase64); - Assertions.assertTrue(signingCert.getDetachedSct().isPresent()); - Assertions.assertEquals(2, signingCert.getCertificates().size()); - } - - @Test - public void testDecode_embedded() - throws SerializationException, IOException, CertificateException { - String certs = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/certWithSct.pem"), - StandardCharsets.UTF_8); - - var signingCert = SigningCertificate.newSigningCertificate(certs, null); - Assertions.assertTrue(signingCert.hasEmbeddedSct()); - Assertions.assertEquals(3, signingCert.getCertificates().size()); - } - - @Test - public void testDecode_derCert() throws CertificateException, IOException { - String certs = - Resources.toString( - Resources.getResource("dev/sigstore/samples/certs/cert.der"), StandardCharsets.UTF_8); - try { - SigningCertificate.decodeCerts(certs); - Assertions.fail("DER certificate was unexpectedly successfully parsed"); - } catch (CertificateParsingException cpe) { - Assertions.assertEquals( - "no valid PEM certificates were found in response from Fulcio", cpe.getMessage()); - } - } - - @Test - public void testDecode_grpc() throws IOException, CertificateException, SerializationException { - var certs = - GrpcTypes.PemToCertificateChain( - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/cert.pem"), - StandardCharsets.UTF_8)); - String sctBase64 = - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/sct.base64"), - StandardCharsets.UTF_8); - var signingCert = - SigningCertificate.newSigningCertificate( - SigningCertificateDetachedSCT.newBuilder() - .setChain(certs) - .setSignedCertificateTimestamp( - ByteString.copyFrom(Base64.getDecoder().decode(sctBase64))) - .build()); - Assertions.assertTrue(signingCert.getDetachedSct().isPresent()); - Assertions.assertEquals(2, signingCert.getCertificates().size()); - } - - @Test - public void testDecode_embeddedGrpc() - throws IOException, CertificateException, SerializationException { - var certs = - GrpcTypes.PemToCertificateChain( - Resources.toString( - Resources.getResource("dev/sigstore/samples/fulcio-response/valid/certWithSct.pem"), - StandardCharsets.UTF_8)); - var signingCert = - SigningCertificate.newSigningCertificate( - SigningCertificateEmbeddedSCT.newBuilder().setChain(certs).build()); - Assertions.assertTrue(signingCert.hasEmbeddedSct()); - Assertions.assertEquals(3, signingCert.getCertificates().size()); - } -} diff --git a/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem b/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem index 133b6e8b..bd5a13be 100644 --- a/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem +++ b/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/fulcio.crt.pem @@ -1,4 +1,18 @@ -----BEGIN CERTIFICATE----- +MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 +7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS +0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB +BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp +KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI +zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR +nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP +mygUY7Ii2zbdCdliiow= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl diff --git a/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/sct.base64 b/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/sct.base64 deleted file mode 100644 index a4afab0f..00000000 --- a/sigstore-java/src/test/resources/dev/sigstore/samples/fulcio-response/valid/sct.base64 +++ /dev/null @@ -1 +0,0 @@ -eyJzY3RfdmVyc2lvbiI6MCwiaWQiOiJDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJPSIsInRpbWVzdGFtcCI6MTY1MDA0Njc0OTgwNiwiZXh0ZW5zaW9ucyI6IiIsInNpZ25hdHVyZSI6IkJBTUFSekJGQWlFQWxMS093dlo3bG9VdHQ4YUdxY01XL3dWS2N5cWQ2NzRZN29zKzZ5S25xU29DSUNsM2tQNXl4ajNxN3NLbDdxM21EKzdaZlUrMTZkUTF0QUJiQlNBdnZZN2QifQ== \ No newline at end of file