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

extras: Add APIs for RSA Blind Signatures #232

Merged
merged 31 commits into from
Jun 26, 2024

Conversation

simonjbeaumont
Copy link
Contributor

@simonjbeaumont simonjbeaumont commented May 21, 2024

Motivation

RFC 9474 defines the RSA Blind Signatures protocol1, which are a useful building block of privacy-preserving schemes.

Modifications

This PR adds the following public API surface to implement the RSA Blind Signatures protocol:

public struct _RSA.BlindSigning.Parameters<H: HashFunction> {}

extension _RSA.BlindSigning.Parameters where H == SHA384 {
    public static let RSABSSA_SHA384_PSS_Randomized: Self<SHA384>
    public static let RSABSSA_SHA384_PSS_Deterministic: Self<SHA384>
    public static let RSABSSA_SHA384_PSSZERO_Randomized: Self<SHA384>
    public static let RSABSSA_SHA384_PSSZERO_Deterministic: Self<SHA384>
}

public struct _RSA.BlindSigning.PrivateKey {
    public typealias Parameters = _RSA.BlindSigning.Parameters<H>

    // ... similar initializers as other protocol-segreated key types, but taking parameters parameter ...

    // ... same properties as other protocol-segregated key types ...

    public func blindSignature<D: DataProtocol>(for message: D) throws -> _RSA.BlindSigning.BlindSignature
}

public struct _RSA.BlindSigning.PublicKey {
    public typealias Parameters = _RSA.BlindSigning.Parameters<H>

    // ... similar initializers as other protocol-segreated key types, but taking parameters parameter ...

    // ... same properties as other protocol-segregated key types ...

    public func prepare<D: DataProtocol>(_ message: D) -> _RSA.BlindSigning.PreparedMessage

    public func blind(_ message: _RSA.BlindSigning.PreparedMessage) throws -> (blindedMessage: Data, blindInverse: _RSA.BlindSigning.BlindInverse)

    public func finalize(_ signature: _RSA.BlindSigning.BlindSignature, for message: _RSA.BlindSigning.PreparedMessage,
 blindInverse: _RSA.BlindSigning.BlindInverse) throws -> _RSA.Signing.RSASignature

    public func isValidSignature(_ signature: _RSA.Signing.RSASignature, for message: _RSA.BlindSigning.PreparedMessage) -> Bool
}

It also adds tests using the test vectors from the RFC, where possible. That is, for each of the operations except for blind, which we don't have the BoringSSL APIs that would allow us to inject the fixed salt value from the test vectors.

While it's possible to implement the server-side operations using Security framework, it does not expose the APIs we would need to implement the client-side operations so, because the goal is to also provide the client-side operations in a subsequent PR, the implementation uses BoringSSL on all platforms.

In order to construct the RSA keys from the test vector parameters, some BoringSSL helpers were added.

Result

New API in the _CryptoExtras module for the the RSA Blind Signatures protocol as defined in RFC 9474, with support for all named variants: for the RSABSSA_SHA384_PSS_Randomized, RSABSSA_SHA384_PSS_Deterministic, RSABSSA_SHA384_PSSZERO_Randomized, and RSABSSA_SHA384_PSSZERO_Deterministic.

Footnotes

  1. https://datatracker.ietf.org/doc/rfc9474/

@simonjbeaumont simonjbeaumont marked this pull request as ready for review May 21, 2024 12:52
@Lukasa Lukasa added the semver/minor Adds new public API. label May 21, 2024
Sources/_CryptoExtras/Util/BoringSSLHelpers.swift Outdated Show resolved Hide resolved
Tests/_CryptoExtrasTests/TestRSABlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA_boring.swift Outdated Show resolved Hide resolved
) == 1 else {
throw CryptoKitError.internalBoringSSLError()
}
precondition(outputCount == signatureByteCount)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test this on enough key sizes that it's always true? (eg. if first bytes are 0, they are not getting trimmed)

Just want to make sure we don't crash if we have an oddly formatted private key generated some day.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. Let me double check on this, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked again at this and we compute signatureByteCount using BoringSSL RSA_size which returns the size of the modulus in bytes (as opposed to the key size). I also confirmed that BoringSSL RSA_sign_raw allocates a buffer of that size and sets out_len to the same.

@simonjbeaumont simonjbeaumont changed the title Server-side APIs for RSA Blind Signatures extras: Add APIs for RSA Blind Signatures Jun 5, 2024
@simonjbeaumont
Copy link
Contributor Author

@Lukasa this is ready for another pass when you have time.

Sources/_CryptoExtras/Util/RandomBytes.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA+BlindSigning.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA_boring.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA_boring.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA_boring.swift Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA_boring.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/RSA/RSA_boring.swift Outdated Show resolved Hide resolved
@simonjbeaumont
Copy link
Contributor Author

@Lukasa OK, thanks for the latest round of feedback. I've addressed all that now, so it's ready for another pass.

@simonjbeaumont
Copy link
Contributor Author

@Lukasa as we discussed, now that we have ArbitraryPrecisionInteger and FiniteFieldArithmeticContext moved to CryptoBoringWrapper, I've replaced the use of manually managed BIGNUM in the implementation with the memory managed types. Hopefully this makes it much easier to be confident in the correctness.

@simonjbeaumont
Copy link
Contributor Author

@swift-server-bot test this please

Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looking really lovely, just a few quick notes here.

}

@inlinable
public static func random(inclusiveMin: UInt64, exclusiveMax: ArbitraryPrecisionInteger) throws -> ArbitraryPrecisionInteger {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we'd spell this with a range, rather than the explicit bounds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is an interesting one! I initially reached for a Range but a range needs to have a homogeneous type and the underlying call we're wrapping does not take two BIGNUM*, but instead one BN_ULONG and one BIGNUM*.

This left me with two options:

  1. Write it in terms of a Range<ArbitraryPrecisionInteger> and fail if the lower bound is too large.
  2. Write it like this.

When possible I thought we should be providing API that prohibits invalid use if we can leverage the type system to do so.

Sources/_CryptoExtras/RSA/RSA_boring.swift Show resolved Hide resolved
guard let inv = try finiteField.inverse(r) else { continue }
return (r, inv)
}
}()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably just as nice to pre-declare r and inv before the loop as var and then bind them repeatedly throughout the loop than to have this anonymous self-invoking closure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've updated it to use var, although I had this construction to ensure a let binding because during the last round of review, in addition to dealing with pointers, one of the comments was keeping track of which state had been computed and which was mutable.

Sources/_CryptoExtras/Util/BoringSSLHelpers.swift Outdated Show resolved Hide resolved
Sources/_CryptoExtras/Util/BoringSSLHelpers.swift Outdated Show resolved Hide resolved
Copy link
Contributor Author

@simonjbeaumont simonjbeaumont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa addressed most of the feedback. The outstanding items that need further discussion are:

guard let inv = try finiteField.inverse(r) else { continue }
return (r, inv)
}
}()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've updated it to use var, although I had this construction to ensure a let binding because during the last round of review, in addition to dealing with pointers, one of the comments was keeping track of which state had been computed and which was mutable.

Sources/_CryptoExtras/RSA/RSA_boring.swift Show resolved Hide resolved
}

@inlinable
public static func random(inclusiveMin: UInt64, exclusiveMax: ArbitraryPrecisionInteger) throws -> ArbitraryPrecisionInteger {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is an interesting one! I initially reached for a Range but a range needs to have a homogeneous type and the underlying call we're wrapping does not take two BIGNUM*, but instead one BN_ULONG and one BIGNUM*.

This left me with two options:

  1. Write it in terms of a Range<ArbitraryPrecisionInteger> and fail if the lower bound is too large.
  2. Write it like this.

When possible I thought we should be providing API that prohibits invalid use if we can leverage the type system to do so.

@Lukasa Lukasa merged commit fdfdc11 into apple:main Jun 26, 2024
9 checks passed

guard result.withUnsafeMutableBignumPointer({ resultPtr in
exclusiveMax.withUnsafeBignumPointer { exclusiveMaxPtr in
CCryptoBoringSSL_BN_rand_range_ex(resultPtr, inclusiveMin, exclusiveMaxPtr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't compile on Android armv7, a 32-bit platform, with the following error from my Android CI:

Sources/CryptoBoringWrapper/ArbitraryPrecisionInteger.swift:444:62: error: cannot convert value of type 'UInt64' to expected argument type 'BN_ULONG' (aka 'UInt32')
                CCryptoBoringSSL_BN_rand_range_ex(resultPtr, inclusiveMin, exclusiveMaxPtr)
                                                             ^
                                                             BN_ULONG(   )

Copy link
Contributor Author

@simonjbeaumont simonjbeaumont Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@finagolfin sorry about that oversight. I'm sure the fix should be pretty minimal but unfortunately I don't have a way of validating it and it's not part of our CI. Presumably the fix here is to take a UInt instead of a UInt64.

Are you able to test this and open a PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I can reproduce this locally when building for watchOS simulator. Let me put up a patch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants