Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Code for generating ecdsa secp256r1 objects #133

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ target/
.vscode/settings.jsonbuilds
builds
.factorypath
.pregenerated-test-key*
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -36,7 +37,8 @@ public abstract class RpkiSignedObject implements CertificateRepositoryObject {

public static final List<String> ALLOWED_SIGNATURE_ALGORITHM_OIDS = Arrays.asList(
SHA256WITHRSA_ENCRYPTION_OID,
RSA_ENCRYPTION_OID
RSA_ENCRYPTION_OID,
X9ObjectIdentifiers.ecdsa_with_SHA256.getId()
);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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())) {
Expand All @@ -76,8 +99,13 @@ public AttributeTable getAttributes(Map parameters) {

private AttributeTable createSignedAttributes(Date signingTime) {
Hashtable<ASN1ObjectIdentifier, Attribute> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -44,11 +45,11 @@ public class AspaCms extends RpkiSignedObject {
* address family received from the customer.
*/
@NonNull
ImmutableSortedSet<ProviderAS> providerASSet;
SortedSet<Asn> providerASSet;

public AspaCms(RpkiSignedObjectInfo cmsObjectData, int version, Asn customerAsn, ImmutableSortedSet<ProviderAS> providerASSet) {
public AspaCms(RpkiSignedObjectInfo cmsObjectData, int version, Asn customerAsn, SortedSet<Asn> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -25,7 +26,7 @@ public class AspaCmsBuilder extends RpkiSignedObjectBuilder {

private Asn customerAsn;

private ImmutableSortedSet<ProviderAS> providerASSet;
private ImmutableSortedSet<Asn> providerASSet;

public AspaCmsBuilder withCertificate(X509ResourceCertificate certificate) {
this.certificate = certificate;
Expand All @@ -42,8 +43,8 @@ public AspaCmsBuilder withCustomerAsn(@NonNull Asn customerAsn) {
return this;
}

public AspaCmsBuilder withProviderASSet(Iterable<? extends ProviderAS> providerASSet) {
this.providerASSet = ImmutableSortedSet.<ProviderAS>naturalOrder().addAll(providerASSet).build();
public AspaCmsBuilder withProviderASSet(Iterable<? extends Asn> providerASSet) {
this.providerASSet = ImmutableSortedSet.<Asn>naturalOrder().addAll(providerASSet).build();
return this;
}

Expand All @@ -57,47 +58,26 @@ public AspaCms build(PrivateKey privateKey) {
public byte[] getEncoded(PrivateKey privateKey) {
return generateCms(certificate.getCertificate(), privateKey, signatureProvider, AspaCms.CONTENT_TYPE, encodeAspa());
}

/**
* <pre>
* 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
* </pre>
*/
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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -32,7 +28,7 @@ public class AspaCmsParser extends RpkiSignedObjectParser {

@CheckForNull
private Asn customerAsn;
private ImmutableSortedSet<ProviderAS> providerASSet = ImmutableSortedSet.of();
private ImmutableSortedSet<Asn> providerASSet = ImmutableSortedSet.of();

@Override
public void parse(ValidationResult result, byte[] encoded) {
Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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);
Expand All @@ -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<Asn> 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));
}

}

This file was deleted.

Loading
Loading