From d73c7cf99bd9153fbcfcf55fd1e41cb3bcc78828 Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Tue, 31 Oct 2023 16:44:05 +0100 Subject: [PATCH 1/6] Access certificate with generic signed object parser --- README.md | 3 +++ .../crypto/cms/GenericRpkiSignedObjectParser.java | 6 ++++++ .../cms/GenericRpkiSignedObjectParserTest.java | 12 ++++++++++++ 3 files changed, 21 insertions(+) diff --git a/README.md b/README.md index efe6ef71..64749492 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ next (snapshot) release, e.g. `1.1-SNAPSHOT` after releasing `1.0`. ## Changelog +## 2023-10-31 1.36 + * Access the certificate for the generic signed object parser. + ## 2023-10-03 1.35 * Build targets JDK 11 * Prefixes in ROAs are sorted by (prefix, maxlength - missing first) diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java index 68a70e68..17c0ad79 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java @@ -4,10 +4,12 @@ import net.ripe.rpki.commons.crypto.cms.ghostbuster.GhostbustersCms; import net.ripe.rpki.commons.crypto.cms.manifest.ManifestCms; import net.ripe.rpki.commons.crypto.cms.roa.RoaCms; +import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate; import net.ripe.rpki.commons.util.RepositoryObjectType; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.joda.time.DateTime; +import java.security.cert.Certificate; import java.util.Optional; import static net.ripe.rpki.commons.util.RepositoryObjectType.*; @@ -17,6 +19,10 @@ public DateTime getSigningTime() { return super.getSigningTime(); } + public X509ResourceCertificate getCertificate() { + return super.getCertificate(); + } + public Optional getRepositoryObjectType() { final ASN1ObjectIdentifier contentType = getContentType(); if (AspaCms.CONTENT_TYPE.equals(contentType)) { diff --git a/src/test/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParserTest.java b/src/test/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParserTest.java index b425536d..9f42d9af 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParserTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParserTest.java @@ -45,6 +45,18 @@ void should_parse_roa() throws IOException { assertThat(parser.getSigningTime()).isEqualTo(DateTime.parse("2011-11-11T01:55:18+00:00")); } + /** + * Parse an invalid object, but still extract validity period and signing time. + */ + @Test + void should_parse_generic() throws IOException { + GenericRpkiSignedObjectParser parser = parse("interop/aspa/BAD-profile-13-AS211321-profile-13.asa"); + + assertThat(parser.getSigningTime()).isEqualTo(DateTime.parse("2021-11-11T11:19:00Z")); + + assertThat(parser.getCertificate().getValidityPeriod().getNotValidBefore()).isEqualTo(DateTime.parse("2021-11-11T11:14:00Z")); + } + private GenericRpkiSignedObjectParser parse(String path) throws IOException { byte[] bytes = Resources.toByteArray(Resources.getResource(path)); From 3677313f933df097a205cda7d8c58d0e5bff629c Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Wed, 1 Nov 2023 08:10:02 +0100 Subject: [PATCH 2/6] Add signed object file-modification time utility --- pom.xml | 6 +- .../commons/crypto/util/SignedObjectUtil.java | 79 +++++++++++++++++++ .../commons/util/SignedObjectUtilTest.java | 46 +++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java create mode 100644 src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java diff --git a/pom.xml b/pom.xml index 44937006..137dc552 100644 --- a/pom.xml +++ b/pom.xml @@ -47,11 +47,11 @@ 1.52 1.74 - 32.0.0-jre + 32.1.2-jre 2.10.13 1.4.20 2.11.0 - 1.18.22 + 1.18.30 @@ -138,7 +138,7 @@ org.mockito mockito-core - 4.2.0 + 5.6.0 test diff --git a/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java b/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java new file mode 100644 index 00000000..a709df88 --- /dev/null +++ b/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java @@ -0,0 +1,79 @@ +package net.ripe.rpki.commons.crypto.util; + +import lombok.Getter; +import lombok.experimental.UtilityClass; +import net.ripe.rpki.commons.crypto.cms.GenericRpkiSignedObjectParser; +import net.ripe.rpki.commons.crypto.crl.X509Crl; +import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificateParser; +import net.ripe.rpki.commons.util.RepositoryObjectType; +import net.ripe.rpki.commons.validation.ValidationResult; +import org.joda.time.Instant; + +import java.net.URI; + +@UtilityClass +public class SignedObjectUtil { + /** + * Extract the creation time from an object following the method described in https://datatracker.ietf.org/doc/draft-timbru-sidrops-publication-server-bcp/00/. + * Note that this uses notBefore for signed objects because this is guaranteed to match between a + * Manifest and its corresponding CRL. + * + * @param uri URL of the object + * @param decoded object bytes + * @return the file creation time of the object + * @throws NoTimeParsedException if creation time could not be extracted. + */ + public static Instant getFileCreationTime(URI uri, byte[] decoded) throws NoTimeParsedException { + + final RepositoryObjectType objectType = RepositoryObjectType.parse(uri.toString()); + try { + switch (objectType) { + case Manifest: + case Aspa: + case Roa: + case Gbr: + var signedObjectParser = new GenericRpkiSignedObjectParser(); + + signedObjectParser.parse(ValidationResult.withLocation(uri), decoded); + + return signedObjectParser.getCertificate().getValidityPeriod().getNotValidBefore().toInstant(); + case Certificate: + X509ResourceCertificateParser x509CertificateParser = new X509ResourceCertificateParser(); + x509CertificateParser.parse(ValidationResult.withLocation(uri), decoded); + final var cert = x509CertificateParser.getCertificate().getCertificate(); + return Instant.ofEpochMilli(cert.getNotBefore().getTime()); + case Crl: + var x509Crl = X509Crl.parseDerEncoded(decoded, ValidationResult.withLocation(uri)); + var crl = x509Crl.getCrl(); + return Instant.ofEpochMilli(crl.getThisUpdate().getTime()); + case Unknown: + default: + throw new NoTimeParsedException(decoded, uri, "Could not determine file type"); + } + } catch (Exception e) { + if (e instanceof NoTimeParsedException) { + throw e; + } + throw new NoTimeParsedException(decoded, uri, "Could not parse object", e); + } + } + + @Getter + public static class NoTimeParsedException extends Exception { + private static final long serialVersionUID = 1L; + + private byte[] decoded; + private URI uri; + public NoTimeParsedException(byte[] decoded, URI uri, String message) { + super(uri.toString() + ": " + message); + this.decoded = decoded; + this.uri = uri; + } + + public NoTimeParsedException(byte[] decoded, URI uri, String message, Throwable cause) { + super(uri.toString() + ": " + message, cause); + this.decoded = decoded; + this.uri = uri; + } + } +} diff --git a/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java b/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java new file mode 100644 index 00000000..2c97f2ed --- /dev/null +++ b/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java @@ -0,0 +1,46 @@ +package net.ripe.rpki.commons.util; + +import com.google.common.io.Resources; +import net.ripe.rpki.commons.crypto.util.SignedObjectUtil; +import org.joda.time.DateTime; +import org.joda.time.Instant; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.IOException; +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SignedObjectUtilTest { + @DisplayName("Should parse the file creation time from RPKI objects") + @ParameterizedTest(name = "{index} => {0} filename={1} expected-creation-time={3} path={2}") + @CsvSource({ + "ASPA, sample.asa, interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa, 2023-06-07T09:08:41+00:00", + // GBR parser has issues + // "GBR, sample.gbr, conformance/root/goodRealGbrNothingIsWrong.gbr, 2023-06-07T09:01:01Z", + // router certificate case is missing due to lack of samples. + "Manifest, sample.mft, conformance/root/root.mft, 2013-10-28T21:24:39Z", + "ROA, sample.roa, interop/rpkid-objects/nI2bsx18I5mlex8lBpY0WSJUYio.roa, 2011-11-11T01:55:18Z", + "'Generic signed object (that does not match object profile)', generic-signed-object.gbr, interop/aspa/BAD-profile-13-AS211321-profile-13.asa, 2021-11-11T11:19:00Z", + }) + void shouldParseObject(String description, String fileName, String path, String modified) throws IOException, SignedObjectUtil.NoTimeParsedException { + Instant creationTime = SignedObjectUtil.getFileCreationTime(URI.create(fileName), Resources.toByteArray(Resources.getResource(path))); + + assertThat(creationTime).isEqualTo(DateTime.parse(modified)); + } + + @Test + void shouldThrowOnUnknown_payload() { + assertThatThrownBy(() -> SignedObjectUtil.getFileCreationTime(URI.create("foo.cer"), new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})) + .isInstanceOf(SignedObjectUtil.NoTimeParsedException.class); + } + @Test + void shouldThrowOnUnknown_extension() { + assertThatThrownBy(() -> SignedObjectUtil.getFileCreationTime(URI.create("foo.xxx"), Resources.toByteArray(Resources.getResource("interop/aspa/BAD-profile-13-AS211321-profile-13.asa")))) + .isInstanceOf(SignedObjectUtil.NoTimeParsedException.class); + } +} From dadb8050e03aa6aa4b4a7fca109bb3c76ac8cac9 Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Wed, 1 Nov 2023 08:17:13 +0100 Subject: [PATCH 3/6] Clarify that certificate becomes more visible --- .../commons/crypto/cms/GenericRpkiSignedObjectParser.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java index 17c0ad79..cd037218 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/GenericRpkiSignedObjectParser.java @@ -19,6 +19,11 @@ public DateTime getSigningTime() { return super.getSigningTime(); } + /** + * Extend visibility of the certificate to make it public. + * @return the certificate. + */ + @Override public X509ResourceCertificate getCertificate() { return super.getCertificate(); } From dd7fecfb5009eea90880637054c7fd8476923a25 Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Wed, 1 Nov 2023 08:25:16 +0100 Subject: [PATCH 4/6] Prefer signing time and describe the trade-off --- .../commons/crypto/util/SignedObjectUtil.java | 16 ++++++++++++---- .../rpki/commons/util/SignedObjectUtilTest.java | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java b/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java index a709df88..9883d400 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/util/SignedObjectUtil.java @@ -14,9 +14,13 @@ @UtilityClass public class SignedObjectUtil { /** - * Extract the creation time from an object following the method described in https://datatracker.ietf.org/doc/draft-timbru-sidrops-publication-server-bcp/00/. - * Note that this uses notBefore for signed objects because this is guaranteed to match between a - * Manifest and its corresponding CRL. + * Extract the creation time from an object. This does not yet follow the method described in + * https://datatracker.ietf.org/doc/draft-timbru-sidrops-publication-server-bcp/00/. It differs in that it uses + * the signing time for RPKI signed objects. This is a trade-off: + * * signing-time is more correct when multi-use EE certificates are present. + * * signing-time likely does not match the modification time of the CRL. + * + * This needs to be revisited in 2024. * * @param uri URL of the object * @param decoded object bytes @@ -35,8 +39,12 @@ public static Instant getFileCreationTime(URI uri, byte[] decoded) throws NoTime var signedObjectParser = new GenericRpkiSignedObjectParser(); signedObjectParser.parse(ValidationResult.withLocation(uri), decoded); + var signingTime = signedObjectParser.getSigningTime(); - return signedObjectParser.getCertificate().getValidityPeriod().getNotValidBefore().toInstant(); + if (signingTime == null) { + return signedObjectParser.getCertificate().getValidityPeriod().getNotValidBefore().toInstant(); + } + return signingTime.toInstant(); case Certificate: X509ResourceCertificateParser x509CertificateParser = new X509ResourceCertificateParser(); x509CertificateParser.parse(ValidationResult.withLocation(uri), decoded); diff --git a/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java b/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java index 2c97f2ed..611eeb82 100644 --- a/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java +++ b/src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java @@ -19,7 +19,7 @@ public class SignedObjectUtilTest { @DisplayName("Should parse the file creation time from RPKI objects") @ParameterizedTest(name = "{index} => {0} filename={1} expected-creation-time={3} path={2}") @CsvSource({ - "ASPA, sample.asa, interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa, 2023-06-07T09:08:41+00:00", + "ASPA, sample.asa, interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa, 2023-06-07T09:08:41Z", // GBR parser has issues // "GBR, sample.gbr, conformance/root/goodRealGbrNothingIsWrong.gbr, 2023-06-07T09:01:01Z", // router certificate case is missing due to lack of samples. From b351fc244d565f777724516bec41e451f8f15c2b Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Wed, 1 Nov 2023 08:28:12 +0100 Subject: [PATCH 5/6] use JDK 21 in CI/CD --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 355cf968..73b43dae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: # test against latest update of each major Java version, as well as specific updates of LTS versions: - java: [ 11, 17 ] + java: [ 11, 17, 21 ] name: Java ${{ matrix.java }} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e4b714fd..c846a94a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 17 ] + java: [ 21 ] steps: - name: Setup java From bbf1bcd3d5ad853b28016db2cf559d36436e7bbd Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Wed, 1 Nov 2023 08:49:37 +0100 Subject: [PATCH 6/6] Update OWASP dependency-check plugin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 137dc552..a817d3b1 100644 --- a/pom.xml +++ b/pom.xml @@ -353,7 +353,7 @@ org.owasp dependency-check-maven - 6.5.0 + 8.4.2