diff --git a/.gitignore b/.gitignore index 1efd009a0..47aab3d86 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ target/ .vscode/settings.jsonbuilds builds .factorypath +.pregenerated-test-key* diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObject.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObject.java index 755abe4c0..ba21c0775 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObject.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObject.java @@ -11,6 +11,7 @@ import net.ripe.rpki.commons.validation.objectvalidators.X509ResourceCertificateParentChildValidator; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; import org.bouncycastle.cms.CMSSignedDataGenerator; import org.joda.time.DateTime; @@ -36,7 +37,8 @@ public abstract class RpkiSignedObject implements CertificateRepositoryObject { public static final List ALLOWED_SIGNATURE_ALGORITHM_OIDS = Arrays.asList( SHA256WITHRSA_ENCRYPTION_OID, - RSA_ENCRYPTION_OID + RSA_ENCRYPTION_OID, + X9ObjectIdentifiers.ecdsa_with_SHA256.getId() ); /** diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObjectBuilder.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObjectBuilder.java index cf8fe9be1..59f5f4172 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObjectBuilder.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/RpkiSignedObjectBuilder.java @@ -1,9 +1,11 @@ package net.ripe.rpki.commons.crypto.cms; +import com.google.common.base.Preconditions; import net.ripe.rpki.commons.crypto.util.BouncyCastleUtil; import net.ripe.rpki.commons.crypto.x509cert.X509CertificateBuilderHelper; import net.ripe.rpki.commons.crypto.x509cert.X509CertificateUtil; import org.apache.commons.lang3.Validate; +import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.cms.Attribute; @@ -35,6 +37,9 @@ import java.util.Hashtable; import java.util.Map; +import static net.ripe.rpki.commons.crypto.x509cert.X509CertificateBuilderHelper.DEFAULT_SIGNATURE_PROVIDER; +import static net.ripe.rpki.commons.crypto.x509cert.X509CertificateBuilderHelper.ECDSA_SIGNATURE_PROVIDER; + public abstract class RpkiSignedObjectBuilder { protected byte[] generateCms(X509Certificate signingCertificate, PrivateKey privateKey, String signatureProvider, ASN1ObjectIdentifier contentTypeOid, byte[] content) { @@ -53,15 +58,33 @@ private byte[] doGenerate(X509Certificate signingCertificate, PrivateKey private Validate.notNull(subjectKeyIdentifier, "certificate must contain SubjectKeyIdentifier extension"); RPKISignedDataGenerator generator = new RPKISignedDataGenerator(); - addSignerInfo(generator, privateKey, signatureProvider, signingCertificate); + String signatureAlgorithm = null; + switch (signingCertificate.getPublicKey().getAlgorithm()) { + case "RSA": + signatureAlgorithm = X509CertificateBuilderHelper.DEFAULT_SIGNATURE_ALGORITHM; + break; + case "EC": + signatureAlgorithm = X509CertificateBuilderHelper.ECDSA_SIGNATURE_ALGORITHM; + if (DEFAULT_SIGNATURE_PROVIDER.equals(signatureProvider)) { + signatureProvider = ECDSA_SIGNATURE_PROVIDER; + } + break; + default: + Preconditions.checkArgument(false, "Not a supported public key type"); + } + Preconditions.checkArgument(!(X509CertificateBuilderHelper.DEFAULT_SIGNATURE_ALGORITHM.equals(signatureAlgorithm) && X509CertificateBuilderHelper.ECDSA_SIGNATURE_PROVIDER.equals(signatureProvider))); + Preconditions.checkArgument(!(X509CertificateBuilderHelper.ECDSA_SIGNATURE_ALGORITHM.equals(signatureAlgorithm) && DEFAULT_SIGNATURE_PROVIDER.equals(signatureProvider))); + + + addSignerInfo(generator, privateKey, signatureProvider, signatureAlgorithm, signingCertificate); generator.addCertificates(new JcaCertStore(Collections.singleton(signingCertificate))); CMSSignedData data = generator.generate(new CMSProcessableByteArray(contentTypeOid, content), true); return data.getEncoded(); } - private void addSignerInfo(RPKISignedDataGenerator generator, PrivateKey privateKey, String signatureProvider, X509Certificate signingCertificate) throws OperatorCreationException { - ContentSigner signer = new JcaContentSignerBuilder(X509CertificateBuilderHelper.DEFAULT_SIGNATURE_ALGORITHM).setProvider(signatureProvider).build(privateKey); + private void addSignerInfo(RPKISignedDataGenerator generator, PrivateKey privateKey, String signatureProvider, String signatureAlgorithm, X509Certificate signingCertificate) throws OperatorCreationException { + ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).setProvider(signatureProvider).build(privateKey); DigestCalculatorProvider digestProvider = BouncyCastleUtil.DIGEST_CALCULATOR_PROVIDER; SignerInfoGenerator gen = new JcaSignerInfoGeneratorBuilder(digestProvider).setSignedAttributeGenerator( new DefaultSignedAttributeTableGenerator(createSignedAttributes(signingCertificate.getNotBefore())) { @@ -76,8 +99,13 @@ public AttributeTable getAttributes(Map parameters) { private AttributeTable createSignedAttributes(Date signingTime) { Hashtable attributes = new Hashtable<>(); + Attribute signingTimeAttribute = new Attribute(CMSAttributes.signingTime, new DERSet(new Time(signingTime))); attributes.put(CMSAttributes.signingTime, signingTimeAttribute); + + Attribute binarySigningTime = new Attribute(CMSAttributes.binarySigningTime, new DERSet(new ASN1Integer(signingTime.toInstant().toEpochMilli() / 1000))); + attributes.put(CMSAttributes.binarySigningTime, binarySigningTime); + return new AttributeTable(attributes); } } diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCms.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCms.java index 68469e46c..03081f471 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCms.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCms.java @@ -11,6 +11,7 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import java.util.Objects; +import java.util.SortedSet; /** * See https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-aspa-profile-07. @@ -44,11 +45,11 @@ public class AspaCms extends RpkiSignedObject { * address family received from the customer. */ @NonNull - ImmutableSortedSet providerASSet; + SortedSet providerASSet; - public AspaCms(RpkiSignedObjectInfo cmsObjectData, int version, Asn customerAsn, ImmutableSortedSet providerASSet) { + public AspaCms(RpkiSignedObjectInfo cmsObjectData, int version, Asn customerAsn, SortedSet providerASSet) { super(cmsObjectData); - Validate.isTrue(version == 0, "version must be 0"); + Validate.isTrue(version == 1, "version must be 1"); this.version = version; this.customerAsn = customerAsn; this.providerASSet = providerASSet; diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsBuilder.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsBuilder.java index 10872761b..940287b63 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsBuilder.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsBuilder.java @@ -11,6 +11,7 @@ import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.DLTaggedObject; import java.security.PrivateKey; @@ -25,7 +26,7 @@ public class AspaCmsBuilder extends RpkiSignedObjectBuilder { private Asn customerAsn; - private ImmutableSortedSet providerASSet; + private ImmutableSortedSet providerASSet; public AspaCmsBuilder withCertificate(X509ResourceCertificate certificate) { this.certificate = certificate; @@ -42,8 +43,8 @@ public AspaCmsBuilder withCustomerAsn(@NonNull Asn customerAsn) { return this; } - public AspaCmsBuilder withProviderASSet(Iterable providerASSet) { - this.providerASSet = ImmutableSortedSet.naturalOrder().addAll(providerASSet).build(); + public AspaCmsBuilder withProviderASSet(Iterable providerASSet) { + this.providerASSet = ImmutableSortedSet.naturalOrder().addAll(providerASSet).build(); return this; } @@ -57,47 +58,26 @@ public AspaCms build(PrivateKey privateKey) { public byte[] getEncoded(PrivateKey privateKey) { return generateCms(certificate.getCertificate(), privateKey, signatureProvider, AspaCms.CONTENT_TYPE, encodeAspa()); } - /** *
+     * ct-ASPA CONTENT-TYPE ::= { TYPE ASProviderAttestation IDENTIFIED BY id-ct-ASPA }
      * ASProviderAttestation ::= SEQUENCE {
-     *   version [0]   ASPAVersion DEFAULT v0,
-     *   customerASID  ASID,
-     *   providers     ProviderASSet
-     * }
-     *
-     * ASPAVersion ::= INTEGER  { v0(0) }
-     *
-     * ProviderASSet ::= SEQUENCE (SIZE(1..MAX)) OF ProviderAS
-     *
-     * ProviderAS ::= SEQUENCE {
-     *   providerASID  ASID,
-     *   afiLimit      AddressFamilyIdentifier OPTIONAL
+     *     version [0]   INTEGER DEFAULT 0,
+     *     customerASID  ASID,
+     *     providers     ProviderASSet
      * }
-     *
-     * ASID ::= INTEGER
-     *
-     * AddressFamilyIdentifier ::= OCTET STRING (SIZE (2))
+     * ProviderASSet ::= SEQUENCE (SIZE(1..MAX)) OF ASID
      * 
*/ private byte[] encodeAspa() { Validate.notNull(customerAsn, "Customer AS ID must not be null"); Validate.notEmpty(providerASSet, "ProviderASSet must not be empty"); ASN1Encodable[] encodables = { - // Version is default value, so must not be encoded + // Version is needs to be 1, but needs to be explicitly tagged + new DLTaggedObject(0, new ASN1Integer(1)), new ASN1Integer(customerAsn.getValue()), - new DERSequence(providerASSet.stream().map(as -> { - if (as.getAfiLimit().isPresent()) { - return new DERSequence(new ASN1Encodable[] { - new ASN1Integer(as.getProviderAsn().getValue()), - as.getAfiLimit().get().toDer() - }); - } else { - return new DERSequence(new ASN1Encodable[] { - new ASN1Integer(as.getProviderAsn().getValue()) - }); - } - }).toArray(ASN1Encodable[]::new)) + new DERSequence(providerASSet.stream().map(as ->new ASN1Integer(as.getValue() + )).toArray(ASN1Encodable[]::new)) }; return Asn1Util.encode(new DERSequence(encodables)); } diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParser.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParser.java index 14e7abfa2..287fe2ef9 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParser.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParser.java @@ -1,25 +1,21 @@ package net.ripe.rpki.commons.crypto.cms.aspa; import com.google.common.base.Joiner; +import com.google.common.collect.Comparators; import com.google.common.collect.ImmutableSortedSet; import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpResourceSet; import net.ripe.rpki.commons.crypto.cms.RpkiSignedObjectInfo; import net.ripe.rpki.commons.crypto.cms.RpkiSignedObjectParser; -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; import net.ripe.rpki.commons.crypto.util.Asn1Util; import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate; import net.ripe.rpki.commons.validation.ValidationResult; import net.ripe.rpki.commons.validation.ValidationString; -import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.ASN1Integer; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.DERTaggedObject; +import org.bouncycastle.asn1.*; import javax.annotation.CheckForNull; import java.util.Comparator; -import java.util.Optional; -import java.util.Set; +import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -32,7 +28,7 @@ public class AspaCmsParser extends RpkiSignedObjectParser { @CheckForNull private Asn customerAsn; - private ImmutableSortedSet providerASSet = ImmutableSortedSet.of(); + private ImmutableSortedSet providerASSet = ImmutableSortedSet.of(); @Override public void parse(ValidationResult result, byte[] encoded) { @@ -65,17 +61,16 @@ private void validateAspa() { ); X509ResourceCertificate resourceCertificate = getCertificate(); - validationResult.rejectIfFalse( - customerAsn != null && - resourceCertificate != null && - resourceCertificate.containsResources(new IpResourceSet(customerAsn)), - ValidationString.ASPA_CUSTOMER_ASN_CERTIFIED - ); - - // * The CustomerASID value MUST NOT appear in any providerASID field if (customerAsn != null) { - boolean providerAsInCustomerAs = providerASSet.stream().map(ProviderAS::getProviderAsn).anyMatch(customerAsn::equals); - validationResult.rejectIfTrue(providerAsInCustomerAs, ASPA_CUSTOMER_ASN_NOT_IN_PROVIDER_ASNS, String.valueOf(customerAsn), Joiner.on(", ").join(providerASSet)); + // Do not reject for customer ASN not being certified if parsing failed earlier. + validationResult.rejectIfFalse( + resourceCertificate != null && + resourceCertificate.containsResources(new IpResourceSet(customerAsn)), + ValidationString.ASPA_CUSTOMER_ASN_CERTIFIED + ); + + // * The CustomerASID value MUST NOT appear in any providerASID field + validationResult.rejectIfTrue(providerASSet.contains(customerAsn), ASPA_CUSTOMER_ASN_NOT_IN_PROVIDER_ASNS, String.valueOf(customerAsn), Joiner.on(", ").join(providerASSet)); } } @@ -93,15 +88,15 @@ public void decodeAsn1Content(ASN1Encodable content) { int index = 0; ASN1Encodable maybeVersion = seq.getObjectAt(index); - if (maybeVersion instanceof DERTaggedObject) { - // Version is optional and defaults to 0, so should not be explicitly encoded when using DER encoding - // If it is present and correct, we still accept the object. If the version is different, reject the - // object. - decodeVersion(validationResult, (DERTaggedObject) maybeVersion); + if (maybeVersion instanceof DLTaggedObject) { + // Version is optional and defaults to 0 if missing. An explicitly tagged integer is present when + // another version is present. + decodeVersion(validationResult, (DLTaggedObject) maybeVersion); ++index; } else { - this.version = 0; + // Other pass/fails for same key are in `decodeVersion` + validationResult.rejectIfFalse(false, ValidationString.ASPA_VERSION, "0 [missing]"); } validationResult.rejectIfFalse(index < itemCount && seq.getObjectAt(index) instanceof ASN1Integer, ValidationString.ASPA_CUSTOMER_ASN_PRESENT); @@ -118,47 +113,40 @@ public void decodeAsn1Content(ASN1Encodable content) { } ASN1Sequence providerAsnsSequence = expect(seq.getObjectAt(index), ASN1Sequence.class); - // TODO: - // * The elements of providers MUST be ordered in ascending numerical - // order by the value of the providerASID field. - // * Each value of providerASID MUST be unique (with respect to the - // other elements of providers). - this.providerASSet = StreamSupport.stream(providerAsnsSequence.spliterator(), false) - .map(this::parseProviderAS) - .collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.naturalOrder())); + + List providerAsList = StreamSupport.stream(providerAsnsSequence.spliterator(), false) + .map(this::parseProviderAsn) + .collect(Collectors.toList()); + + // * The elements of providers MUST be ordered in ascending numerical + // order.ΒΆ + validationResult.rejectIfFalse(Comparators.isInStrictOrder(providerAsList, Comparator.naturalOrder()), ValidationString.ASPA_PROVIDER_AS_SET_VALID, "elements are in order"); + + if (validationResult.hasFailureForCurrentLocation()) { + return; + } + + this.providerASSet = ImmutableSortedSet.copyOf(providerAsList); + // * Each value of providerASID MUST be unique (with respect to the + // other elements of providers). + validationResult.rejectIfFalse(providerASSet.size() == providerAsnsSequence.size(), ValidationString.ASPA_PROVIDER_AS_SET_VALID, "elements are unique"); validationResult.rejectIfTrue(providerASSet.isEmpty(), ValidationString.ASPA_PROVIDER_AS_SET_NOT_EMPTY); } catch (IllegalArgumentException ex) { validationResult.error(ValidationString.ASPA_CONTENT_STRUCTURE); } } - private void decodeVersion(ValidationResult validationResult, DERTaggedObject tagged) { + private void decodeVersion(ValidationResult validationResult, DLTaggedObject tagged) { validationResult.rejectIfFalse(tagged.getTagNo() == 0, ValidationString.ASPA_CONTENT_STRUCTURE); try { this.version = expect(tagged.getBaseObject(), ASN1Integer.class).intValueExact(); - validationResult.rejectIfFalse(this.version == 0, ValidationString.ASPA_VERSION, String.valueOf(this.version)); + validationResult.rejectIfFalse(this.version == 1, ValidationString.ASPA_VERSION, String.valueOf(this.version)); } catch (ArithmeticException e) { validationResult.error(ValidationString.ASPA_VERSION, "out-of-bounds"); } } - private ProviderAS parseProviderAS(ASN1Encodable asn1Encodable) { - ValidationResult validationResult = getValidationResult(); - ASN1Sequence sequence = expect(asn1Encodable, ASN1Sequence.class); - - validationResult.rejectIfTrue(sequence.size() < 1 || sequence.size() > 2, ValidationString.ASPA_PROVIDER_AS_SEQUENCE_SIZE); - if (validationResult.hasFailureForCurrentLocation()) { - throw new IllegalArgumentException("invalid sequence length"); - } - - Asn providerAsn = Asn1Util.parseAsId(sequence.getObjectAt(0)); - AddressFamily afiLimit = null; - if (sequence.size() > 1) { - afiLimit = AddressFamily.fromDer(sequence.getObjectAt(1)); - validationResult.rejectIfFalse(afiLimit.equals(AddressFamily.IPV4) || afiLimit.equals(AddressFamily.IPV6), ValidationString.ASPA_ADDR_FAMILY); - } - - return new ProviderAS(providerAsn, Optional.ofNullable(afiLimit)); + private Asn parseProviderAsn(ASN1Encodable asn1Encodable) { + return Asn1Util.parseAsId(expect(asn1Encodable, ASN1Integer.class)); } - } diff --git a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/ProviderAS.java b/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/ProviderAS.java deleted file mode 100644 index 9cf198fa8..000000000 --- a/src/main/java/net/ripe/rpki/commons/crypto/cms/aspa/ProviderAS.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.ripe.rpki.commons.crypto.cms.aspa; - -import lombok.NonNull; -import lombok.Value; -import net.ripe.ipresource.Asn; -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; - -import java.util.Objects; -import java.util.Optional; - -@Value -public class ProviderAS implements Comparable { - @NonNull Asn providerAsn; - @NonNull Optional afiLimit; - - @Override - public int compareTo(ProviderAS that) { - int rc = this.providerAsn.compareTo(that.providerAsn); - if (rc != 0) { - return rc; - } - - if (Objects.equals(this.afiLimit, that.afiLimit)) { - return 0; - } else if (!this.afiLimit.isPresent()) { - return -1; - } else if (!that.afiLimit.isPresent()) { - return 1; - } else { - return this.afiLimit.get().compareTo(that.afiLimit.get()); - } - } -} diff --git a/src/main/java/net/ripe/rpki/commons/crypto/util/EcKeyPairFactory.java b/src/main/java/net/ripe/rpki/commons/crypto/util/EcKeyPairFactory.java new file mode 100644 index 000000000..e10823c71 --- /dev/null +++ b/src/main/java/net/ripe/rpki/commons/crypto/util/EcKeyPairFactory.java @@ -0,0 +1,58 @@ +package net.ripe.rpki.commons.crypto.util; + +import java.math.BigInteger; +import java.security.*; +import java.security.spec.*; + +public class EcKeyPairFactory { + public static final String ALGORITHM = "EC"; + + private final String provider; + + public EcKeyPairFactory(String provider) { + this.provider = provider; + } + + public KeyPair generate() { + try { + final KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM, provider); + generator.initialize(new ECGenParameterSpec("secp256r1")); + return generator.generateKeyPair(); + } catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new KeyPairFactoryException(e); + } + } + + /** + * Decodes an X.509 encoded public key. + * + * @param encoded the encoded public key. + * @return the PublicKey. + */ + public static PublicKey decodePublicKey(byte[] encoded) { + try { + return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new KeyPairFactoryException(e); + } + } + + /** + * Decodes a PKCS#8 encoded private key. This is the default encoding for + * the private key getEncoded method. + * + * @param encoded the encoded data. + * @return the PrivateKey. + */ + public static PrivateKey decodePrivateKey(byte[] encoded) { + try { + return KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new KeyPairFactoryException(e); + } + } + + public EcKeyPairFactory withProvider(String provider) { + return new EcKeyPairFactory(provider); + } +} diff --git a/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateBuilderHelper.java b/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateBuilderHelper.java index cf76fbf1b..48739ce22 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateBuilderHelper.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateBuilderHelper.java @@ -1,5 +1,6 @@ package net.ripe.rpki.commons.crypto.x509cert; +import lombok.Getter; import net.ripe.ipresource.IpResourceSet; import net.ripe.ipresource.IpResourceType; import net.ripe.rpki.commons.crypto.ValidityPeriod; @@ -67,13 +68,19 @@ public final class X509CertificateBuilderHelper { public static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withRSA"; + public static final String ECDSA_SIGNATURE_ALGORITHM = "SHA256withECDSA"; + public static final String DEFAULT_SIGNATURE_PROVIDER = "SunRsaSign"; + public static final String ECDSA_SIGNATURE_PROVIDER = "SunEC"; + private static final BigInteger MAX_20_OCTETS = BigInteger.ONE.shiftLeft(160).subtract(BigInteger.ONE); - private String signatureProvider = DEFAULT_SIGNATURE_PROVIDER; + @Getter + private String signatureProvider; - private String signatureAlgorithm = DEFAULT_SIGNATURE_ALGORITHM; + @Getter + private String signatureAlgorithm; private BigInteger serial; @@ -87,6 +94,7 @@ public final class X509CertificateBuilderHelper { private PublicKey publicKey; + @Getter private KeyPair signingKeyPair; private int keyUsage; @@ -334,7 +342,7 @@ private void validateCertificateFields() { Validate.notNull(publicKey, "no publicKey"); Validate.notNull(signingKeyPair, "no signingKeyPair"); Validate.notNull(validityPeriod, "no validityPeriod"); - Validate.isTrue("RSA".equals(publicKey.getAlgorithm()), "publicKey algorithm is not RSA"); + Validate.isTrue("RSA".equals(publicKey.getAlgorithm()) || "EC".equals(publicKey.getAlgorithm()), "publicKey algorithm is not RSA or EC"); if (!ca) { Validate.isTrue((keyUsage & KeyUsage.keyCertSign) == 0, "keyCertSign only allowed for ca"); diff --git a/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateParser.java b/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateParser.java index af79c5dc1..3c12446a1 100644 --- a/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateParser.java +++ b/src/main/java/net/ripe/rpki/commons/crypto/x509cert/X509CertificateParser.java @@ -5,6 +5,7 @@ import net.ripe.rpki.commons.validation.ValidationResult; import org.apache.commons.lang3.ArrayUtils; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -28,6 +29,7 @@ public abstract class X509CertificateParser { + public AsnGen() { + super(Asn.class); + } + + @Override + public Asn generate(SourceOfRandomness random, GenerationStatus status) { + return new Asn(Integer.toUnsignedLong(random.nextInt())); + } +} 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 3c4ede3dd..b425536d3 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 @@ -14,10 +14,10 @@ public class GenericRpkiSignedObjectParserTest { @Test void should_parse_aspa() throws IOException { - GenericRpkiSignedObjectParser parser = parse("interop/aspa/AS211321.asa"); + GenericRpkiSignedObjectParser parser = parse("interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa"); assertThat(parser.getRepositoryObjectType().get()).isEqualTo(RepositoryObjectType.Aspa); - assertThat(parser.getSigningTime()).isEqualTo(DateTime.parse("2021-11-11T11:19:00+00:00")); + assertThat(parser.getSigningTime()).isEqualTo(DateTime.parse("2023-06-07T09:08:41+00:00")); } @Disabled("Our parser rejects GBR objects: corrupted stream - out of bounds length found: 115 >= 32") diff --git a/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParserTest.java b/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParserTest.java index ca2ee0c0c..5b83b0e9e 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParserTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsParserTest.java @@ -2,39 +2,80 @@ import com.google.common.io.Resources; import net.ripe.ipresource.Asn; -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; import net.ripe.rpki.commons.validation.ValidationResult; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.util.Optional; -import static net.ripe.rpki.commons.validation.ValidationString.ASPA_CONTENT_TYPE; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static net.ripe.rpki.commons.validation.ValidationString.*; +import static org.assertj.core.api.Assertions.assertThat; class AspaCmsParserTest { @Test - void should_parse_aspa_rpkimancer() throws IOException { - AspaCms aspa = parseValidAspa("interop/aspa/aspa-rpkimancer.asa"); - assertThat(aspa.getCustomerAsn()).isEqualTo(Asn.parse("AS65000")); + void should_parse_draft15_aspa() throws IOException { + AspaCms aspa = parseValidAspa("interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa"); + assertThat(aspa.getCustomerAsn()).isEqualTo(Asn.parse("AS15562")); + assertThat(aspa.getVersion()).isEqualTo(1); assertThat(aspa.getProviderASSet()).containsExactly( - new ProviderAS(Asn.parse("AS65001"), Optional.empty()), - new ProviderAS(Asn.parse("AS65002"), Optional.of(AddressFamily.IPV4)) + Asn.parse("AS2914"), + Asn.parse("AS8283"), + Asn.parse("AS51088"), + Asn.parse("206238") ); } @Test - void should_parse_aspa_krill() throws IOException { - AspaCms aspa = parseValidAspa("interop/aspa/AS211321.asa"); - assertThat(aspa.getCustomerAsn()).isEqualTo(Asn.parse("AS211321")); + void should_parse_draft15_rpki_commons_object() throws IOException { + AspaCms aspa = parseValidAspa("interop/aspa/GOOD-profile-15-rpki-commons-propertytest-sample.asa"); + assertThat(aspa.getVersion()).isEqualTo(1); + } + + @Test + void should_accept_apnic_test_aspa_v1() throws IOException { + AspaCms aspa = parseValidAspa("interop/aspa/GOOD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa"); + assertThat(aspa.getCustomerAsn()).isEqualTo(Asn.parse("AS1000")); + assertThat(aspa.getVersion()).isEqualTo(1); assertThat(aspa.getProviderASSet()).containsExactly( - new ProviderAS(Asn.parse("AS65000"), Optional.empty()), - new ProviderAS(Asn.parse("AS65001"), Optional.of(AddressFamily.IPV4)), - new ProviderAS(Asn.parse("AS65002"), Optional.of(AddressFamily.IPV6)) + Asn.parse("AS1025") ); } + /** + * draft 15 mentions implicit tags by accident, this was changed without a OID change later. + */ + @Test + void should_reject_draft15_rpki_commons_with_implicit_tag() throws IOException { + AspaCmsParser parser = new AspaCmsParser(); + ValidationResult result = ValidationResult.withLocation("BAD-profile-15-rpki-commons-propertytest-sample-implicit-tag.asa"); + parser.parse(result, Resources.toByteArray(Resources.getResource("interop/aspa/BAD-profile-15-rpki-commons-propertytest-sample-implicit-tag.asa"))); + + assertThat(result.hasFailures()).isTrue(); + // Content structure check should fail because tags are wrong + assertThat(result.getFailuresForAllLocations()).anyMatch(check -> ASPA_CONTENT_STRUCTURE.equals(check.getKey())); + } + + @Test + void should_reject_apnic_test_aspa_WRONG_VERSION() throws IOException { + AspaCmsParser parser = new AspaCmsParser(); + ValidationResult result = ValidationResult.withLocation("BAD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa"); + parser.parse(result, Resources.toByteArray(Resources.getResource("interop/aspa/BAD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa"))); + + assertThat(result.hasFailures()).isTrue(); + assertThat(result.getFailuresForAllLocations()).anyMatch(check -> ASPA_VERSION.equals(check.getKey())); + } + + @Test + void parseAspa_wrong_profile_version() throws IOException { + AspaCmsParser parser = new AspaCmsParser(); + ValidationResult result = ValidationResult.withLocation("BAD-profile-13-AS211321-profile-13.asa"); + parser.parse(result, Resources.toByteArray(Resources.getResource("interop/aspa/BAD-profile-13-AS211321-profile-13.asa"))); + + assertThat(result.hasFailures()).isTrue(); + // Abort after wrong version + assertThat(result.getFailuresForAllLocations()).allMatch(check -> ASPA_VERSION.equals(check.getKey())); + } + private AspaCms parseValidAspa(String path) throws IOException { byte[] bytes = Resources.toByteArray(Resources.getResource(path)); ValidationResult result = ValidationResult.withLocation(path); diff --git a/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsTest.java b/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsTest.java index b088468f8..863ae932f 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/AspaCmsTest.java @@ -1,39 +1,39 @@ package net.ripe.rpki.commons.crypto.cms.aspa; import com.google.common.collect.ImmutableSortedSet; +import com.google.common.io.BaseEncoding; +import com.pholser.junit.quickcheck.From; import com.pholser.junit.quickcheck.Property; import com.pholser.junit.quickcheck.When; import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; import net.ripe.ipresource.Asn; +import net.ripe.ipresource.AsnGen; import net.ripe.ipresource.IpResourceSet; import net.ripe.rpki.commons.crypto.ValidityPeriod; -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; import net.ripe.rpki.commons.crypto.util.KeyPairFactoryTest; import net.ripe.rpki.commons.crypto.x509cert.X509CertificateInformationAccessDescriptor; import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate; import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificateBuilder; import net.ripe.rpki.commons.util.UTC; -import net.ripe.rpki.commons.validation.ValidationResult; -import org.apache.commons.io.FileUtils; import org.bouncycastle.asn1.x509.KeyUsage; import org.joda.time.DateTime; import org.junit.Test; import org.junit.runner.RunWith; import javax.security.auth.x500.X500Principal; -import java.io.File; -import java.io.IOException; import java.math.BigInteger; import java.net.URI; import java.security.KeyPair; import java.util.Comparator; import java.util.List; -import java.util.Optional; +import static net.ripe.rpki.commons.crypto.util.KeyPairFactoryTest.EC_TEST_KEY_PAIR; import static net.ripe.rpki.commons.crypto.x509cert.X509CertificateBuilderHelper.DEFAULT_SIGNATURE_PROVIDER; +import static net.ripe.rpki.commons.crypto.x509cert.X509CertificateBuilderHelper.ECDSA_SIGNATURE_PROVIDER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.*; +import static org.junit.Assume.assumeThat; @RunWith(JUnitQuickcheck.class) public class AspaCmsTest { @@ -45,10 +45,9 @@ public class AspaCmsTest { private static final URI TEST_CA_LOCATION = URI.create("rsync://certificate/repository/ca.cer"); private static final BigInteger ROA_CERT_SERIAL = BigInteger.TEN; - private static final ImmutableSortedSet PROVIDER_AS_SET = ImmutableSortedSet.naturalOrder() - .add(new ProviderAS(Asn.parse("AS65001"), Optional.empty())) - .add(new ProviderAS(Asn.parse("AS65002"), Optional.of(AddressFamily.IPV4))) - .build(); + private static final ImmutableSortedSet PROVIDER_AS_SET = ImmutableSortedSet.of( + Asn.parse("AS65001"), Asn.parse("AS65002") + ); private static final Asn CUSTOMER_ASN = Asn.parse("AS65000"); @@ -62,35 +61,37 @@ public void should_reject_creating_aspa_with_empty_provider_as_set() { } @Property(trials = 100) - public void should_generate_aspa(int customerAsId, @When(satisfies = "!#_.isEmpty") List providerAsIdSet) { + public void should_generate_aspa(int customerAsId, @When(satisfies = "!#_.isEmpty") List providerAsIdSet) { Asn customerAsn = new Asn(Integer.toUnsignedLong(customerAsId)); - ImmutableSortedSet providerAsSet = providerAsIdSet.stream() + ImmutableSortedSet providerAsSet = providerAsIdSet.stream() .collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.naturalOrder())); AspaCms cms = createAspa(customerAsn, providerAsSet); - assertEquals(0, cms.getVersion()); + assertEquals(1, cms.getVersion()); assertEquals(customerAsn, cms.getCustomerAsn()); assertEquals(providerAsSet, cms.getProviderASSet()); + + System.out.println(BaseEncoding.base64().encode(cms.getEncoded())); } public static AspaCms createAspa() { return createAspa(CUSTOMER_ASN, PROVIDER_AS_SET); } - public static AspaCms createAspa(Asn customerAsn, ImmutableSortedSet providerAsSet) { + public static AspaCms createAspa(Asn customerAsn, ImmutableSortedSet providerAsSet) { AspaCmsBuilder builder = new AspaCmsBuilder(); builder.withCertificate(createCertificate(new IpResourceSet(customerAsn))); builder.withCustomerAsn(customerAsn); builder.withProviderASSet( providerAsSet ); - builder.withSignatureProvider(DEFAULT_SIGNATURE_PROVIDER); - return builder.build(TEST_KEY_PAIR.getPrivate()); + builder.withSignatureProvider(ECDSA_SIGNATURE_PROVIDER); + return builder.build(EC_TEST_KEY_PAIR.getPrivate()); } private static X509ResourceCertificate createCertificate(IpResourceSet resources) { X509ResourceCertificateBuilder builder = new X509ResourceCertificateBuilder(); builder.withCa(false).withIssuerDN(TEST_DN).withSubjectDN(TEST_DN).withSerial(ROA_CERT_SERIAL); - builder.withPublicKey(TEST_KEY_PAIR.getPublic()); + builder.withPublicKey(EC_TEST_KEY_PAIR.getPublic()); builder.withSigningKeyPair(TEST_KEY_PAIR); final DateTime now = UTC.dateTime(); builder.withKeyUsage(KeyUsage.digitalSignature); diff --git a/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/ProviderASGen.java b/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/ProviderASGen.java deleted file mode 100644 index ebcf05980..000000000 --- a/src/test/java/net/ripe/rpki/commons/crypto/cms/aspa/ProviderASGen.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.ripe.rpki.commons.crypto.cms.aspa; - -import com.pholser.junit.quickcheck.generator.GenerationStatus; -import com.pholser.junit.quickcheck.generator.Generator; -import com.pholser.junit.quickcheck.random.SourceOfRandomness; -import net.ripe.ipresource.Asn; -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; - -import java.util.Arrays; -import java.util.Optional; - -/** - * Automatically used by {@link com.pholser.junit.quickcheck.runner.JUnitQuickcheck}. - */ -@SuppressWarnings("unused") -public class ProviderASGen extends Generator { - public ProviderASGen() { - super(ProviderAS.class); - } - - @Override - public ProviderAS generate(SourceOfRandomness random, GenerationStatus status) { - Asn providerAsn = new Asn(Integer.toUnsignedLong(random.nextInt())); - Optional afiLimit = random.choose(Arrays.asList(Optional.empty())); - return new ProviderAS(providerAsn, afiLimit); - } -} diff --git a/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsBuilderTest.java b/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsBuilderTest.java index e200ce3eb..60710b569 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsBuilderTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsBuilderTest.java @@ -1,6 +1,7 @@ package net.ripe.rpki.commons.crypto.cms.roa; import com.google.common.collect.ImmutableSortedSet; +import com.google.common.io.BaseEncoding; import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; import net.ripe.rpki.commons.crypto.util.KeyPairFactoryTest; import org.junit.Before; @@ -36,14 +37,15 @@ public void setUp() { subject.withCertificate(createCertificate(allPrefixes)); subject.withAsn(TEST_ASN); subject.withPrefixes(allPrefixes); - subject.withSignatureProvider(DEFAULT_SIGNATURE_PROVIDER); + subject.withSignatureProvider(ECDSA_SIGNATURE_PROVIDER); } @Test public void shouldGenerateRoaCms() { - RoaCms result = subject.build(KeyPairFactoryTest.TEST_KEY_PAIR.getPrivate()); + RoaCms result = subject.build(KeyPairFactoryTest.EC_TEST_KEY_PAIR.getPrivate()); assertNotNull(result); assertNotNull(result.getEncoded()); + System.out.println(BaseEncoding.base64().encode(result.getEncoded())); } @Test diff --git a/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsTest.java b/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsTest.java index 5fabae5ed..172bb589c 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/cms/roa/RoaCmsTest.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import static net.ripe.rpki.commons.crypto.cms.roa.RoaCmsParserTest.*; +import static net.ripe.rpki.commons.crypto.util.KeyPairFactoryTest.EC_TEST_KEY_PAIR; import static net.ripe.rpki.commons.crypto.x509cert.X509CertificateBuilderHelper.*; import static org.assertj.core.api.Assertions.assertThat; @@ -65,9 +66,9 @@ public static RoaCms createRoaCms(List prefixes) { RoaCmsBuilder builder = new RoaCmsBuilder(); builder.withCertificate(createCertificate(prefixes)).withAsn(TEST_ASN); builder.withPrefixes(prefixes); - builder.withSignatureProvider(DEFAULT_SIGNATURE_PROVIDER); + builder.withSignatureProvider(ECDSA_SIGNATURE_PROVIDER); - return builder.build(TEST_KEY_PAIR.getPrivate()); + return builder.build(EC_TEST_KEY_PAIR.getPrivate()); } // TODO: Refactor to RoaCmsObjectMother @@ -78,7 +79,7 @@ public static RoaCms getRoaCms() { } public static X509ResourceCertificate createCertificate(List prefixes){ - return createCertificate(prefixes, TEST_KEY_PAIR); + return createCertificate(prefixes, EC_TEST_KEY_PAIR); } public static X509ResourceCertificate createCertificate(List prefixes, KeyPair keyPair) { IpResourceSet resources = new IpResourceSet(); @@ -90,7 +91,7 @@ public static X509ResourceCertificate createCertificate(List prefixes } private static X509ResourceCertificateBuilder createCertificateBuilder(IpResourceSet resources) { - return createCertificateBuilder(resources, TEST_KEY_PAIR); + return createCertificateBuilder(resources, EC_TEST_KEY_PAIR); } private static X509ResourceCertificateBuilder createCertificateBuilder(IpResourceSet resources, KeyPair keyPair) { X509ResourceCertificateBuilder builder = new X509ResourceCertificateBuilder(); diff --git a/src/test/java/net/ripe/rpki/commons/crypto/util/Asn1UtilTest.java b/src/test/java/net/ripe/rpki/commons/crypto/util/Asn1UtilTest.java index bf2b722d6..676bd7932 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/util/Asn1UtilTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/util/Asn1UtilTest.java @@ -8,48 +8,52 @@ import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DERBitString; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static net.ripe.rpki.commons.crypto.rfc3779.ResourceExtensionEncoderTest.*; import static net.ripe.rpki.commons.crypto.util.Asn1Util.*; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class Asn1UtilTest { - @Test(expected = IllegalArgumentException.class) + @Test public void shouldFailToParseNonZeroPadBits() { byte[] WRONG_ENCODED_IPV4_10_5_0_0_23 = {0x03, 0x04, 0x01, 0x0a, 0x05, 0x01}; - parseIpAddressAsPrefix(IpResourceType.IPv4, decode(WRONG_ENCODED_IPV4_10_5_0_0_23)); + assertThatThrownBy(() -> parseIpAddressAsPrefix(IpResourceType.IPv4, decode(WRONG_ENCODED_IPV4_10_5_0_0_23))) + .isIn(IllegalArgumentException.class); } - @Test(expected = Asn1UtilException.class) + @Test public void shouldFailIPv4ParsingWhenNoValidDerBitStringFoundP() { // bouncy castle 1.70+ catches this invalid case when decoding byte[] WRONG_ENCODED_IPV4_10_5_0_0_23 = {0x05, 0x04, 0x01, 0x0a, 0x05, 0x01}; - parseIpAddressAsPrefix(IpResourceType.IPv4, decode(WRONG_ENCODED_IPV4_10_5_0_0_23)); + assertThatThrownBy(() -> parseIpAddressAsPrefix(IpResourceType.IPv4, decode(WRONG_ENCODED_IPV4_10_5_0_0_23))) + .isInstanceOf(Asn1UtilException.class); } @Test public void shouldParseIpv4Address() { - assertEquals(IpResource.parse("0.0.0.0/0"), parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_0_0_0_0_0))); - assertEquals(IpResource.parse("10.5.0.4/32"), parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_5_0_4_32))); - assertEquals(IpResource.parse("10.5.0.0/23"), parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_5_0_0_23))); - assertEquals(IpResource.parse("10.64.0.0/12"), parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_64_0_0_12))); - assertEquals(IpResource.parse("10.64.0.0/20"), parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_64_0_0_20))); - assertEquals(IpResource.parse("128.5.0.4/32"), parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_128_5_0_4_32))); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_0_0_0_0_0))).isEqualTo(IpResource.parse("0.0.0.0/0")); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_5_0_4_32))).isEqualTo(IpResource.parse("10.5.0.4/32")); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_5_0_0_23))).isEqualTo(IpResource.parse("10.5.0.0/23")); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_64_0_0_12))).isEqualTo(IpResource.parse("10.64.0.0/12")); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_10_64_0_0_20))).isEqualTo(IpResource.parse("10.64.0.0/20")); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv4, decode(ENCODED_IPV4_128_5_0_4_32))).isEqualTo(IpResource.parse("128.5.0.4/32")); } - @Test(expected = IllegalArgumentException.class) + @Test public void shouldFailToParseNonZeroPadBitsIpv4Address() { byte[] WRONG_ENCODED_IPV4_10_5_0_0 = {0x03, 0x03, 0x01, 0x0a, 0x05}; - parseIpAddress(IpResourceType.IPv4, decode(WRONG_ENCODED_IPV4_10_5_0_0), false); + assertThatThrownBy(() -> parseIpAddress(IpResourceType.IPv4, decode(WRONG_ENCODED_IPV4_10_5_0_0), false)) + .isInstanceOf(IllegalArgumentException.class); } @Test public void shouldParseIpv6Addresses() { - assertEquals(IpResource.parse("2001:0:200:3::1/128"), - parseIpAddressAsPrefix(IpResourceType.IPv6, decode(ENCODED_IPV6_2001_0_200_3_0_0_0_1_128))); - assertEquals(IpResource.parse("2001:0:200::/39"), parseIpAddressAsPrefix(IpResourceType.IPv6, decode(ENCODED_IPV6_2001_0_200_39))); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv6, decode(ENCODED_IPV6_2001_0_200_3_0_0_0_1_128))) + .isEqualTo(IpResource.parse("2001:0:200:3::1/128")); + assertThat(parseIpAddressAsPrefix(IpResourceType.IPv6, decode(ENCODED_IPV6_2001_0_200_39))).isEqualTo(IpResource.parse("2001:0:200::/39")); } @Test @@ -78,16 +82,17 @@ public void shouldEncodeIpv6Prefix() { @Test public void shouldDecodeAsn() { - assertEquals(ASN_0, parseAsId(decode(ENCODED_ASN_0))); - assertEquals(ASN_127, parseAsId(decode(ENCODED_ASN_127))); - assertEquals(ASN_128, parseAsId(decode(ENCODED_ASN_128))); - assertEquals(ASN_412_233, parseAsId(decode(ENCODED_ASN_412_233))); - assertEquals(ASN_65535_65535, parseAsId(decode(ENCODED_ASN_65535_65535))); + assertThat(parseAsId(decode(ENCODED_ASN_0))).isEqualTo(ASN_0); + assertThat(parseAsId(decode(ENCODED_ASN_127))).isEqualTo(ASN_127); + assertThat(parseAsId(decode(ENCODED_ASN_128))).isEqualTo(ASN_128); + assertThat(parseAsId(decode(ENCODED_ASN_412_233))).isEqualTo(ASN_412_233); + assertThat(parseAsId(decode(ENCODED_ASN_65535_65535))).isEqualTo(ASN_65535_65535); } - @Test(expected = IllegalArgumentException.class) + @Test public void shouldFailOnOutOfRangeAsn() { - parseAsId(new ASN1Integer(-1)); + assertThatThrownBy(() -> parseAsId(new ASN1Integer(-1))) + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -100,12 +105,12 @@ public void HandleZeroSlashEight() { IpRange ipAfter = Asn1Util.parseIpAddressAsPrefix(IpResourceType.IPv4, bitString); String actual = ipAfter.toString(); - assertEquals("The ip addresses should not have mutated!", expected + "/16", actual); + assertThat(actual).isEqualTo(expected + "/16") + .withFailMessage("The ip addresses should not have mutated!"); } public static void assertEncoded(byte[] expected, ASN1Encodable encodable) { byte[] actual = encode(encodable); - assertArrayEquals(expected, actual); + assertThat(actual).isEqualTo(expected); } - } diff --git a/src/test/java/net/ripe/rpki/commons/crypto/util/CertificateRepositoryObjectFactoryTest.java b/src/test/java/net/ripe/rpki/commons/crypto/util/CertificateRepositoryObjectFactoryTest.java index 1fca877fa..7bcb4e721 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/util/CertificateRepositoryObjectFactoryTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/util/CertificateRepositoryObjectFactoryTest.java @@ -180,7 +180,7 @@ public void shouldParseAspa() { assertTrue(object instanceof AspaCms); assertEquals(aspa, object); - assertEquals("" + validationResult.getAllValidationChecksForCurrentLocation(), 62, validationResult.getAllValidationChecksForCurrentLocation().size()); + assertEquals("" + validationResult.getAllValidationChecksForCurrentLocation(), 63, validationResult.getAllValidationChecksForCurrentLocation().size()); assertTrue("" + validationResult.getAllValidationChecksForCurrentLocation(), validationResult.hasNoFailuresOrWarnings()); assertTrue(validationResult.getResultForCurrentLocation(KNOWN_OBJECT_TYPE).isOk()); assertTrue(validationResult.getResultForCurrentLocation(ASPA_CUSTOMER_ASN_CERTIFIED).isOk()); @@ -194,7 +194,7 @@ public void shouldParseMalformedAspa() { CertificateRepositoryObject object = createCertificateRepositoryObject(encoded, validationResult); assertNull(object); - assertEquals("" + validationResult.getAllValidationChecksForCurrentLocation(), 4, validationResult.getAllValidationChecksForCurrentLocation().size()); + assertEquals("" + validationResult.getAllValidationChecksForCurrentLocation(), 3, validationResult.getAllValidationChecksForCurrentLocation().size()); assertFalse("" + validationResult.getAllValidationChecksForCurrentLocation(), validationResult.hasNoFailuresOrWarnings()); assertTrue(validationResult.getResultForCurrentLocation(KNOWN_OBJECT_TYPE).isOk()); } diff --git a/src/test/java/net/ripe/rpki/commons/crypto/util/KeyPairFactoryTest.java b/src/test/java/net/ripe/rpki/commons/crypto/util/KeyPairFactoryTest.java index a94f03152..e697fd410 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/util/KeyPairFactoryTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/util/KeyPairFactoryTest.java @@ -3,39 +3,41 @@ import org.junit.Test; import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; -import java.util.HashMap; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import static org.junit.Assert.*; public class KeyPairFactoryTest { - - public static final String DEFAULT_KEYPAIR_GENERATOR_PROVIDER = "SunRsaSign"; + public static final String EC_KEYPAIR_GENERATOR_PROVIDER = "SunEC"; + public static final String RSA_KEYPAIR_GENERATOR_PROVIDER = "SunRsaSign"; public static KeyPair TEST_KEY_PAIR = PregeneratedKeyPairFactory.getInstance().generate(); public static KeyPair SECOND_TEST_KEY_PAIR = PregeneratedKeyPairFactory.getInstance().generate(); - private static final Map cachedKeyPairs = new HashMap(); + public static KeyPair EC_TEST_KEY_PAIR = PregeneratedEcKeyPairFactory.getInstance().generate();; + public static KeyPair EC_SECOND_TEST_KEY_PAIR = PregeneratedEcKeyPairFactory.getInstance().generate(); + + private static final ConcurrentHashMap cachedKeyPairs = new ConcurrentHashMap(); + private static final ConcurrentHashMap cachedEcKeyPairs = new ConcurrentHashMap(); public static KeyPair getKeyPair(String name) { - synchronized (cachedKeyPairs) { - KeyPair result = cachedKeyPairs.get(name); - if (result == null) { - result = PregeneratedKeyPairFactory.getInstance().generate(); - cachedKeyPairs.put(name, result); - } - return result; - } + return cachedKeyPairs.computeIfAbsent(name, unused -> PregeneratedKeyPairFactory.getInstance().generate()); + } + + public static KeyPair getEcKeyPair(String name) { + return cachedEcKeyPairs.computeIfAbsent(name, unused -> PregeneratedEcKeyPairFactory.getInstance().generate()); } @Test public void shouldGenerateRsaKeyPairs() { - KeyPair keyPair = new KeyPairFactory(DEFAULT_KEYPAIR_GENERATOR_PROVIDER).generate(); + KeyPair keyPair = new KeyPairFactory(RSA_KEYPAIR_GENERATOR_PROVIDER).generate(); assertTrue(keyPair.getPublic() instanceof RSAPublicKey); assertTrue(keyPair.getPrivate() instanceof RSAPrivateKey); @@ -47,6 +49,19 @@ public void shouldGenerateRsaKeyPairs() { assertEquals(KeyPairFactory.RPKI_KEY_PAIR_SIZE, rsaPublicKey.getModulus().bitLength()); } + @Test + public void shouldGenerateEcdsaKeyPairs() { + KeyPair keyPair = new EcKeyPairFactory(EC_KEYPAIR_GENERATOR_PROVIDER).generate(); + assertTrue(keyPair.getPublic() instanceof ECPublicKey); + assertTrue(keyPair.getPrivate() instanceof ECPrivateKey); + + assertEquals(keyPair.getPublic(), EcKeyPairFactory.decodePublicKey(keyPair.getPublic().getEncoded())); + assertEquals(keyPair.getPrivate(), EcKeyPairFactory.decodePrivateKey(keyPair.getPrivate().getEncoded())); + + ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic(); + assertEquals("EC", ecPublicKey.getAlgorithm()); + } + @Test(expected = RuntimeException.class) public void shouldKeypairGenerationFailOnInvalidProvider() { new KeyPairFactory("invalid_provider").generate(); diff --git a/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedEcKeyPairFactory.java b/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedEcKeyPairFactory.java new file mode 100644 index 000000000..8fdcc12ac --- /dev/null +++ b/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedEcKeyPairFactory.java @@ -0,0 +1,115 @@ +package net.ripe.rpki.commons.crypto.util; + +import net.ripe.ipresource.IpResourceSet; +import net.ripe.rpki.commons.crypto.ValidityPeriod; +import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate; +import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificateBuilder; +import net.ripe.rpki.commons.util.UTC; +import org.joda.time.DateTime; + +import javax.security.auth.x500.X500Principal; +import java.io.*; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; + +/** + * Caches generated keys in a key store so that they can be reused in the next test run. FOR TESTING ONLY! + */ +public final class PregeneratedEcKeyPairFactory extends EcKeyPairFactory { + + private static final PregeneratedEcKeyPairFactory INSTANCE = new PregeneratedEcKeyPairFactory("SunEC"); + + private static final char[] PASSPHRASE = "passphrase".toCharArray(); + + private File keyStoreFile; + + private KeyStore pregeneratedKeys; + + private int count = 0; + + private PregeneratedEcKeyPairFactory(String provider) { + super(provider); + keyStoreFile = new File(".pregenerated-test-key-pairs-ec-" + provider + ".keystore"); + initKeyStore(); + } + + private void initKeyStore() { + try { + pregeneratedKeys = KeyStore.getInstance("JKS", "SUN"); + try (InputStream input = new FileInputStream(keyStoreFile)) { + pregeneratedKeys.load(input, PASSPHRASE); + } catch (FileNotFoundException e) { + pregeneratedKeys.load(null, PASSPHRASE); + } + } catch (final IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + public static PregeneratedEcKeyPairFactory getInstance() { + return INSTANCE; + } + + @Override + public EcKeyPairFactory withProvider(String provider) { + return this; + } + + @Override + public synchronized KeyPair generate() { + try { + String alias = "key_" + count; + ++count; + + PrivateKey key = (PrivateKey) pregeneratedKeys.getKey(alias, PASSPHRASE); + KeyPair result; + if (key == null) { + result = super.generate(); + pregeneratedKeys.setKeyEntry(alias, result.getPrivate(), PASSPHRASE, new Certificate[]{createCertificate(result).getCertificate()}); + try (final OutputStream output = new FileOutputStream(keyStoreFile)) { + pregeneratedKeys.store(output, PASSPHRASE); + } + } else { + Certificate certificate = pregeneratedKeys.getCertificateChain(alias)[0]; + result = new KeyPair(certificate.getPublicKey(), key); + } + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static X509ResourceCertificate createCertificate(KeyPair keyPair) { + X509ResourceCertificateBuilder builder = new X509ResourceCertificateBuilder(); + builder.withSignatureProvider("SunEC"); + builder.withSerial(BigInteger.ONE); + final DateTime now = UTC.dateTime(); + builder.withValidityPeriod(new ValidityPeriod(now.minusYears(2), now.minusYears(1))); + builder.withCa(false); + builder.withIssuerDN(new X500Principal("CN=issuer")); + builder.withSubjectDN(new X500Principal("CN=subject")); + builder.withResources(IpResourceSet.parse("AS1-AS10,10/8,ffc0::/16")); + builder.withSigningKeyPair(keyPair); + builder.withPublicKey(keyPair.getPublic()); + return builder.build(); + } + + private static X509ResourceCertificate createEcCertificate(KeyPair keyPair) { + X509ResourceCertificateBuilder builder = new X509ResourceCertificateBuilder(); + builder.withSignatureProvider("EC"); + builder.withSerial(BigInteger.ONE); + final DateTime now = UTC.dateTime(); + builder.withValidityPeriod(new ValidityPeriod(now.minusYears(2), now.minusYears(1))); + builder.withCa(false); + builder.withIssuerDN(new X500Principal("CN=issuer")); + builder.withSubjectDN(new X500Principal("CN=subject")); + builder.withResources(IpResourceSet.parse("AS1-AS10,10/8,ffc0::/16")); + builder.withSigningKeyPair(keyPair); + builder.withPublicKey(keyPair.getPublic()); + return builder.build(); + } +} diff --git a/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedKeyPairFactory.java b/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedKeyPairFactory.java index ff63887bf..4f661efc1 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedKeyPairFactory.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/util/PregeneratedKeyPairFactory.java @@ -31,13 +31,15 @@ public final class PregeneratedKeyPairFactory extends KeyPairFactory { private static final char[] PASSPHRASE = "passphrase".toCharArray(); - private File keyStoreFile = new File(".pregenerated-test-key-pairs.keystore"); + private File keyStoreFile; + private KeyStore pregeneratedKeys; private int count = 0; private PregeneratedKeyPairFactory(String provider) { super(provider); + keyStoreFile = new File(".pregenerated-test-key-pairs-" + provider + ".keystore"); initKeyStore(); } @@ -101,4 +103,19 @@ private static X509ResourceCertificate createCertificate(KeyPair keyPair) { builder.withPublicKey(keyPair.getPublic()); return builder.build(); } + + private static X509ResourceCertificate createEcCertificate(KeyPair keyPair) { + X509ResourceCertificateBuilder builder = new X509ResourceCertificateBuilder(); + builder.withSignatureProvider("EC"); + builder.withSerial(BigInteger.ONE); + final DateTime now = UTC.dateTime(); + builder.withValidityPeriod(new ValidityPeriod(now.minusYears(2), now.minusYears(1))); + builder.withCa(false); + builder.withIssuerDN(new X500Principal("CN=issuer")); + builder.withSubjectDN(new X500Principal("CN=subject")); + builder.withResources(IpResourceSet.parse("AS1-AS10,10/8,ffc0::/16")); + builder.withSigningKeyPair(keyPair); + builder.withPublicKey(keyPair.getPublic()); + return builder.build(); + } } diff --git a/src/test/java/net/ripe/rpki/commons/crypto/x509cert/X509ResourceCertificateTest.java b/src/test/java/net/ripe/rpki/commons/crypto/x509cert/X509ResourceCertificateTest.java index d0ea501e9..813a9d7d2 100644 --- a/src/test/java/net/ripe/rpki/commons/crypto/x509cert/X509ResourceCertificateTest.java +++ b/src/test/java/net/ripe/rpki/commons/crypto/x509cert/X509ResourceCertificateTest.java @@ -1,5 +1,6 @@ package net.ripe.rpki.commons.crypto.x509cert; +import com.google.common.io.BaseEncoding; import net.ripe.ipresource.IpResourceSet; import net.ripe.ipresource.IpResourceType; import net.ripe.rpki.commons.crypto.ValidityPeriod; @@ -14,6 +15,7 @@ import net.ripe.rpki.commons.validation.ValidationString; import net.ripe.rpki.commons.validation.objectvalidators.CertificateRepositoryObjectValidationContext; import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.util.encoders.Base64; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Ignore; @@ -75,8 +77,8 @@ public static X509ResourceCertificateBuilder createBasicBuilder() { builder.withIssuerDN(TEST_SELF_SIGNED_CERTIFICATE_NAME); builder.withSerial(TEST_SERIAL_NUMBER); builder.withValidityPeriod(TEST_VALIDITY_PERIOD); - builder.withPublicKey(KeyPairFactoryTest.TEST_KEY_PAIR.getPublic()); - builder.withSigningKeyPair(KeyPairFactoryTest.TEST_KEY_PAIR); + builder.withPublicKey(KeyPairFactoryTest.EC_TEST_KEY_PAIR.getPublic()); + builder.withSigningKeyPair(KeyPairFactoryTest.EC_TEST_KEY_PAIR); builder.withAuthorityKeyIdentifier(true); X509CertificateInformationAccessDescriptor[] descriptors = { @@ -183,6 +185,30 @@ public void shouldSupportCaCertificate() { assertFalse(cert.isEe()); } + @Test + public void shouldSupportCaAndLeafCertificate() { + X509ResourceCertificate root = createSelfSignedCaResourceCertificateBuilder().build(); + assertFalse(root.isEe()); + assertTrue(root.isCa()); + + System.out.println("-----BEGIN CERTIFICATE-----"); + System.out.println(BaseEncoding.base64().encode(root.getEncoded())); + System.out.println("-----END CERTIFICATE-----"); + + var leafDn = new X500Principal("CN=EE"); + + X509ResourceCertificate cert = createSelfSignedEeCertificateBuilder() + .withSubjectDN(leafDn) + .withPublicKey(KeyPairFactoryTest.SECOND_TEST_KEY_PAIR.getPublic()) + .build(); + assertFalse(cert.isCa()); + assertTrue(cert.isEe()); + + System.out.println("-----BEGIN CERTIFICATE-----"); + System.out.println(BaseEncoding.base64().encode(cert.getEncoded())); + System.out.println("-----END CERTIFICATE-----"); + } + @Test public void shouldSupportAuthorityInformationAccessExtension() throws URISyntaxException { X509CertificateInformationAccessDescriptor[] descriptors = { diff --git a/src/test/resources/interop/aspa/AS211321.asa b/src/test/resources/interop/aspa/BAD-profile-13-AS211321-profile-13.asa similarity index 100% rename from src/test/resources/interop/aspa/AS211321.asa rename to src/test/resources/interop/aspa/BAD-profile-13-AS211321-profile-13.asa diff --git a/src/test/resources/interop/aspa/aspa-rpkimancer.asa b/src/test/resources/interop/aspa/BAD-profile-13-no-signingtime-aspa-rpkimancer.asa similarity index 100% rename from src/test/resources/interop/aspa/aspa-rpkimancer.asa rename to src/test/resources/interop/aspa/BAD-profile-13-no-signingtime-aspa-rpkimancer.asa diff --git a/src/test/resources/interop/aspa/BAD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa b/src/test/resources/interop/aspa/BAD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa new file mode 100644 index 000000000..594cc446b Binary files /dev/null and b/src/test/resources/interop/aspa/BAD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa differ diff --git a/src/test/resources/interop/aspa/BAD-profile-15-rpki-commons-propertytest-sample-implicit-tag.asa b/src/test/resources/interop/aspa/BAD-profile-15-rpki-commons-propertytest-sample-implicit-tag.asa new file mode 100644 index 000000000..407c3f343 Binary files /dev/null and b/src/test/resources/interop/aspa/BAD-profile-15-rpki-commons-propertytest-sample-implicit-tag.asa differ diff --git a/src/test/resources/interop/aspa/GOOD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa b/src/test/resources/interop/aspa/GOOD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa new file mode 100644 index 000000000..88421771f Binary files /dev/null and b/src/test/resources/interop/aspa/GOOD-profile-15-APNIC-rpki-aspa-demo-AS1000.asa differ diff --git a/src/test/resources/interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa b/src/test/resources/interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa new file mode 100644 index 000000000..e9884ecfe Binary files /dev/null and b/src/test/resources/interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa differ diff --git a/src/test/resources/interop/aspa/GOOD-profile-15-rpki-commons-propertytest-sample.asa b/src/test/resources/interop/aspa/GOOD-profile-15-rpki-commons-propertytest-sample.asa new file mode 100644 index 000000000..f517599dc Binary files /dev/null and b/src/test/resources/interop/aspa/GOOD-profile-15-rpki-commons-propertytest-sample.asa differ