Skip to content

Commit

Permalink
feat(SignedData): Support SubjectKeyIdentifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea committed Aug 5, 2020
1 parent c370850 commit 600c9bc
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 34 deletions.
77 changes: 65 additions & 12 deletions src/main/kotlin/tech/relaycorp/relaynet/crypto/SignedData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.bouncycastle.cms.CMSProcessableByteArray
import org.bouncycastle.cms.CMSSignedData
import org.bouncycastle.cms.CMSSignedDataGenerator
import org.bouncycastle.cms.CMSTypedData
import org.bouncycastle.cms.SignerInfoGenerator
import org.bouncycastle.cms.SignerInformation
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder
Expand Down Expand Up @@ -39,6 +40,7 @@ class SignedData(internal val bcSignedData: CMSSignedData) {
*/
val signerCertificate: Certificate? by lazy {
val signerInfo = getSignerInfo(bcSignedData)

// We shouldn't have to force this type cast but this is the only way I could get the code to work and, based on
// what I found online, that's what others have had to do as well
@Suppress("UNCHECKED_CAST") val signerCertSelector = X509CertificateHolderSelector(
Expand Down Expand Up @@ -81,12 +83,12 @@ class SignedData(internal val bcSignedData: CMSSignedData) {
?: expectedPlaintext
?: throw SignedDataException("Plaintext should be encapsulated or explicitly set")

if (this.signerCertificate != null && signerPublicKey != null) {
if (signerCertificate != null && signerPublicKey != null) {
throw SignedDataException(
"No specific signer certificate should be expected because one is already " +
"encapsulated"
)
} else if (this.signerCertificate == null && signerPublicKey == null) {
} else if (signerCertificate == null && signerPublicKey == null) {
throw SignedDataException(
"Signer certificate should be encapsulated or explicitly set"
)
Expand All @@ -97,8 +99,8 @@ class SignedData(internal val bcSignedData: CMSSignedData) {
)
val signerInfo = getSignerInfo(signedData)
val verifierBuilder = JcaSimpleSignerInfoVerifierBuilder().setProvider(BC_PROVIDER)
val verifier = if (this.signerCertificate != null)
verifierBuilder.build(this.signerCertificate!!.certificateHolder)
val verifier = if (signerCertificate != null)
verifierBuilder.build(signerCertificate!!.certificateHolder)
else
verifierBuilder.build(signerPublicKey)
try {
Expand All @@ -115,6 +117,9 @@ class SignedData(internal val bcSignedData: CMSSignedData) {
HashingAlgorithm.SHA512 to "SHA512WITHRSAANDMGF1"
)

/**
* Generate SignedData value with a SignerInfo using an IssuerAndSerialNumber id.
*/
@JvmStatic
fun sign(
plaintext: ByteArray,
Expand All @@ -123,17 +128,51 @@ class SignedData(internal val bcSignedData: CMSSignedData) {
encapsulatedCertificates: Set<Certificate> = setOf(),
hashingAlgorithm: HashingAlgorithm? = null,
encapsulatePlaintext: Boolean = true
): SignedData {
val contentSigner = makeContentSigner(signerPrivateKey, hashingAlgorithm)
val signerInfoGenerator = makeSignerInfoGeneratorBuilder().build(
contentSigner,
signerCertificate.certificateHolder
)
return sign(
plaintext,
signerInfoGenerator,
encapsulatedCertificates,
encapsulatePlaintext
)
}

/**
* Generate SignedData value with a SignerInfo using a SubjectKeyIdentifier.
*/
@JvmStatic
fun sign(
plaintext: ByteArray,
signerPrivateKey: PrivateKey,
hashingAlgorithm: HashingAlgorithm? = null,
encapsulatePlaintext: Boolean = true
): SignedData {
val contentSigner = makeContentSigner(signerPrivateKey, hashingAlgorithm)
val signerInfoGenerator = makeSignerInfoGeneratorBuilder().build(
contentSigner,
byteArrayOf()
)
return sign(
plaintext,
signerInfoGenerator,
emptySet(),
encapsulatePlaintext
)
}

private fun sign(
plaintext: ByteArray,
signerInfoGenerator: SignerInfoGenerator,
encapsulatedCertificates: Set<Certificate>,
encapsulatePlaintext: Boolean
): SignedData {
val signedDataGenerator = CMSSignedDataGenerator()

val algorithm = hashingAlgorithm ?: HashingAlgorithm.SHA256
val signerBuilder =
JcaContentSignerBuilder(signatureAlgorithmMap[algorithm]).setProvider(BC_PROVIDER)
val contentSigner: ContentSigner = signerBuilder.build(signerPrivateKey)
val signerInfoGenerator = JcaSignerInfoGeneratorBuilder(
JcaDigestCalculatorProviderBuilder()
.build()
).build(contentSigner, signerCertificate.certificateHolder)
signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator)

val certs = JcaCertStore(encapsulatedCertificates.map { it.certificateHolder })
Expand All @@ -149,6 +188,20 @@ class SignedData(internal val bcSignedData: CMSSignedData) {
)
}

private fun makeSignerInfoGeneratorBuilder() = JcaSignerInfoGeneratorBuilder(
JcaDigestCalculatorProviderBuilder().build()
)

private fun makeContentSigner(
signerPrivateKey: PrivateKey,
hashingAlgorithm: HashingAlgorithm?
): ContentSigner {
val algorithm = hashingAlgorithm ?: HashingAlgorithm.SHA256
val signerBuilder =
JcaContentSignerBuilder(signatureAlgorithmMap[algorithm]).setProvider(BC_PROVIDER)
return signerBuilder.build(signerPrivateKey)
}

@JvmStatic
fun deserialize(serialization: ByteArray): SignedData {
val asn1Stream = ASN1InputStream(serialization)
Expand Down
119 changes: 97 additions & 22 deletions src/test/kotlin/tech/relaycorp/relaynet/crypto/SignedDataTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class SignedDataTest {
@Nested
inner class Plaintext {
@Test
fun `Plaintext should be encapsulated by default`() {
fun `Plaintext should be encapsulated by default when using certificates`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
Expand All @@ -160,7 +160,7 @@ class SignedDataTest {
}

@Test
fun `Plaintext should not be encapsulated if requested`() {
fun `Plaintext should not be encapsulated if requested when using certificates`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
Expand All @@ -170,6 +170,25 @@ class SignedDataTest {

assertNull(signedData.plaintext)
}

@Test
fun `Plaintext should be encapsulated by default when not using certificates`() {
val signedData = SignedData.sign(stubPlaintext, stubKeyPair.private)

assertNotNull(signedData.plaintext)
assertEquals(stubPlaintext.asList(), signedData.plaintext!!.asList())
}

@Test
fun `Plaintext should not be encapsulated if requested when not using certificates`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
encapsulatePlaintext = false
)

assertNull(signedData.plaintext)
}
}

@Nested
Expand All @@ -186,7 +205,7 @@ class SignedDataTest {
}

@Test
fun `SignerInfo version should be set to 1`() {
fun `SignerInfo version should be set to 1 when signed with a certificate`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
Expand All @@ -198,7 +217,7 @@ class SignedDataTest {
}

@Test
fun `SignerIdentifier should be IssuerAndSerialNumber`() {
fun `SignerIdentifier should be IssuerAndSerialNumber when using a certificate`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
Expand All @@ -213,6 +232,27 @@ class SignedDataTest {
)
}

@Test
fun `SignerInfo version should be set to 3 when signed without a certificate`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
stubCertificate
)

val signerInfo = signedData.bcSignedData.signerInfos.first()
assertEquals(1, signerInfo.version)
}

@Test
fun `SignerIdentifier should be SubjectKeyIdentifier when not using a certificate`() {
val signedData = SignedData.sign(stubPlaintext, stubKeyPair.private)

val signerInfo = signedData.bcSignedData.signerInfos.first()
assertNull(signerInfo.sid.issuer)
assertEquals(byteArrayOf().asList(), signerInfo.sid.subjectKeyIdentifier.asList())
}

@Test
fun `Signature algorithm should be RSA-PSS`() {
val signedData = SignedData.sign(
Expand Down Expand Up @@ -300,6 +340,13 @@ class SignedDataTest {
assertEquals(0, signedData.certificates.size)
}

@Test
fun `Signer certificate should not be encapsulated when not using certificates`() {
val signedData = SignedData.sign(stubPlaintext, stubKeyPair.private)

assertEquals(0, signedData.certificates.size)
}

@Test
fun `CA certificate chain should optionally be encapsulated`() {
val signedData = SignedData.sign(
Expand All @@ -317,48 +364,69 @@ class SignedDataTest {
@Nested
inner class Hashing {
@Test
fun `SHA-256 should be used by default`() {
fun `SHA-256 should be used by default when using certificates`() {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
stubCertificate
)

assertEquals(1, signedData.bcSignedData.digestAlgorithmIDs.size)
assertEquals(
HASHING_ALGORITHM_OIDS[HashingAlgorithm.SHA256],
signedData.bcSignedData.digestAlgorithmIDs.first().algorithm
assertHashingAlgoEquals(signedData, HashingAlgorithm.SHA256)
}

@ParameterizedTest(name = "{0} should be honored if explicitly set")
@EnumSource
fun `Hashing algorithm should be customizable when using certificates`(
algorithm: HashingAlgorithm
) {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
stubCertificate,
hashingAlgorithm = algorithm
)

val signerInfo = signedData.bcSignedData.signerInfos.first()
assertHashingAlgoEquals(signedData, algorithm)
}

assertEquals(
HASHING_ALGORITHM_OIDS[HashingAlgorithm.SHA256],
signerInfo.digestAlgorithmID.algorithm
)
@Test
fun `SHA-256 should be used by default when not using certificates`() {
val signedData = SignedData.sign(stubPlaintext, stubKeyPair.private)

assertHashingAlgoEquals(signedData, HashingAlgorithm.SHA256)
}

@ParameterizedTest(name = "{0} should be honored if explicitly set")
@EnumSource
fun `Hashing algorithm should be customizable`(algorithm: HashingAlgorithm) {
fun `Hashing algorithm should be customizable when not using certificates`(
algorithm: HashingAlgorithm
) {
val signedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
stubCertificate,
hashingAlgorithm = algorithm
)

val hashingAlgorithmOid = HASHING_ALGORITHM_OIDS[algorithm]
assertHashingAlgoEquals(signedData, algorithm)
}

private fun assertHashingAlgoEquals(
signedData: SignedData,
expectedHashingAlgorithm: HashingAlgorithm
) {
val expectedHashingAlgoOID = HASHING_ALGORITHM_OIDS[expectedHashingAlgorithm]

assertEquals(1, signedData.bcSignedData.digestAlgorithmIDs.size)
assertEquals(
hashingAlgorithmOid,
expectedHashingAlgoOID,
signedData.bcSignedData.digestAlgorithmIDs.first().algorithm
)

val signerInfo = signedData.bcSignedData.signerInfos.first()

assertEquals(hashingAlgorithmOid, signerInfo.digestAlgorithmID.algorithm)
assertEquals(
expectedHashingAlgoOID,
signerInfo.digestAlgorithmID.algorithm
)
}
}
}
Expand Down Expand Up @@ -522,7 +590,7 @@ class SignedDataTest {
}

@Test
fun `Valid signature with encapsulated signer certificate should be accepted`() {
fun `Valid signature with encapsulated signer certificate should succeed`() {
val cmsSignedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
Expand All @@ -534,7 +602,7 @@ class SignedDataTest {
}

@Test
fun `Valid signature with explicit signer key should be accepted`() {
fun `Valid signature with explicit signer key should succeed when using certs`() {
val cmsSignedData = SignedData.sign(
stubPlaintext,
stubKeyPair.private,
Expand All @@ -543,6 +611,13 @@ class SignedDataTest {

cmsSignedData.verify(signerPublicKey = stubKeyPair.public)
}

@Test
fun `Valid signature with explicit signer key should succeed when not using certs`() {
val cmsSignedData = SignedData.sign(stubPlaintext, stubKeyPair.private)

cmsSignedData.verify(signerPublicKey = stubKeyPair.public)
}
}

@Nested
Expand Down

0 comments on commit 600c9bc

Please sign in to comment.