This repository has been archived by the owner on Jan 14, 2022. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Alter the API of Salmon related functions
This commit has the following significant changes for the API * Rename symbols with the name the specification uses. * Introduce MagicEnvelope object in favor of reuse of parsed information and simplicity. * Add a method to decode magic public key.
- Loading branch information
1 parent
eec4c51
commit ab9c018
Showing
11 changed files
with
364 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,33 @@ | ||
module OStatus2 | ||
class Salmon | ||
include OStatus2::MagicKey | ||
|
||
XMLNS = 'http://salmon-protocol.org/ns/magic-env' | ||
|
||
# Create a magical envelope XML document around the original body | ||
# and sign it with a private key | ||
# @param [String] body | ||
# @param [OpenSSL::PKey::RSA] key The private part of the key will be used | ||
# @return [String] Magical envelope XML | ||
def pack(body, key) | ||
signed = plaintext_signature(body, 'application/atom+xml', 'base64url', 'RSA-SHA256') | ||
signature = Base64.urlsafe_encode64(key.sign(digest, signed)) | ||
require 'ostatus2/salmon/magic_envelope' | ||
require 'ostatus2/salmon/magic_public_key' | ||
|
||
Nokogiri::XML::Builder.new do |xml| | ||
xml['me'].env({ 'xmlns:me' => XMLNS }) do | ||
xml['me'].data({ type: 'application/atom+xml' }, Base64.urlsafe_encode64(body)) | ||
xml['me'].encoding('base64url') | ||
xml['me'].alg('RSA-SHA256') | ||
xml['me'].sig({ key_id: Base64.urlsafe_encode64(key.public_key.to_s) }, signature) | ||
module OStatus2 | ||
# Provides features related to Salmon, conforming to draft-panzer-magicsig-00 | ||
module Salmon | ||
class << self | ||
# Decode base64url, adding paddings if missing due to conformance with an obsolete specification | ||
# @param [String] string String encoded with base64url, possibly missing paddings | ||
# @return [String] Decoded string | ||
def decode_base64url(string) | ||
retries = 0 | ||
|
||
begin | ||
return Base64::urlsafe_decode64(string) | ||
rescue ArgumentError | ||
retries += 1 | ||
string = "#{string}=" | ||
retry unless retries > 2 | ||
end | ||
end.to_xml | ||
end | ||
|
||
# Deliver the magical envelope to a Salmon endpoint | ||
# @param [String] salmon_url Salmon endpoint URL | ||
# @param [String] envelope Magical envelope | ||
# @raise [HTTP::Error] Error raised upon delivery failure | ||
# @raise [OpenSSL::SSL::SSLError] Error raised upon SSL-related failure during delivery | ||
# @return [HTTP::Response] | ||
def post(salmon_url, envelope) | ||
http_client.headers(HTTP::Headers::CONTENT_TYPE => 'application/magic-envelope+xml').post(Addressable::URI.parse(salmon_url), body: envelope) | ||
end | ||
|
||
# Unpack a magical envelope to get the content inside | ||
# @param [String] raw_body Magical envelope | ||
# @raise [OStatus2::BadSalmonError] Error raised if the envelope is malformed | ||
# @return [String] Content inside the envelope | ||
def unpack(raw_body) | ||
body, _, _ = parse(raw_body) | ||
body | ||
end | ||
|
||
# Verify the magical envelope's integrity | ||
# @param [String] raw_body Magical envelope | ||
# @param [OpenSSL::PKey::RSA] key The public part of the key will be used | ||
# @return [Boolean] | ||
def verify(raw_body, key) | ||
_, plaintext, signature = parse(raw_body) | ||
key.public_key.verify(digest, signature, plaintext) | ||
rescue OStatus2::BadSalmonError | ||
false | ||
end | ||
|
||
private | ||
|
||
def http_client | ||
HTTP.timeout(:per_operation, write: 60, connect: 20, read: 60) | ||
end | ||
|
||
def digest | ||
OpenSSL::Digest::SHA256.new | ||
end | ||
|
||
def parse(raw_body) | ||
xml = Nokogiri::XML(raw_body) | ||
|
||
raise OStatus2::BadSalmonError if xml.at_xpath('//me:data', me: XMLNS).nil? || xml.at_xpath('//me:data', me: XMLNS).attribute('type').nil? || xml.at_xpath('//me:sig', me: XMLNS).nil? || xml.at_xpath('//me:encoding', me: XMLNS).nil? || xml.at_xpath('//me:alg', me: XMLNS).nil? | ||
|
||
data = xml.at_xpath('//me:data', me: XMLNS) | ||
type = data.attribute('type').value | ||
body = decode_base64(data.content.gsub(/\s+/, '')) | ||
sig = xml.at_xpath('//me:sig', me: XMLNS) | ||
signature = decode_base64(sig.content.gsub(/\s+/, '')) | ||
encoding = xml.at_xpath('//me:encoding', me: XMLNS).content | ||
alg = xml.at_xpath('//me:alg', me: XMLNS).content | ||
plaintext = plaintext_signature(body, type, encoding, alg) | ||
|
||
[body, plaintext, signature] | ||
end | ||
|
||
def plaintext_signature(data, type, encoding, alg) | ||
[data, type, encoding, alg].map { |i| Base64.urlsafe_encode64(i) }.join('.') | ||
end | ||
|
||
# Encode base64url (provided for symmetry with decode_base64url. | ||
# see Base64.urlsafe_encode64.) | ||
# @param (see Base64.urlsafe_encode64) | ||
# @raise (see Base64.urlsafe_encode64) | ||
# @return (see Base64.urlsafe_encode64) | ||
def encode_base64url(*args) | ||
Base64.urlsafe_encode64 *args | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
module OStatus2 | ||
module Salmon | ||
# Represents magic envelope | ||
MagicEnvelope = Struct.new(:body, :sig, :key_id, :type, :encoding, :alg) do | ||
# XML Namespace of magic envelope | ||
XMLNS = 'http://salmon-protocol.org/ns/magic-env' | ||
|
||
# @!method initialize(body, sig, key_id, type='application/atom+xml', encoding='base64url', alg='RSA-SHA256') | ||
# @param [String] body Content of data element or complete magic envelope XML serialization as the only argument | ||
# @param [String, OpenSSL::PKey::RSA] sig A signature or a RSA key pair to sign | ||
# @param [String] key_id key_id attribute of alg element | ||
# (defaults to a PEM representation of a public key if provided) | ||
# @param [String] type type attribute of data element | ||
# @param [String] encoding Content of encoding element | ||
# @param [String] alg Content of alg element | ||
# @raise [OStatus2::Salmon::BadError] Error raised if the envelope is malformed | ||
def initialize(*args) | ||
if args.size == 1 | ||
xml = Nokogiri::XML(args[0]) | ||
|
||
raise BadError if xml.at_xpath('//me:data', me: XMLNS).nil? || xml.at_xpath('//me:data', me: XMLNS).attribute('type').nil? || xml.at_xpath('//me:sig', me: XMLNS).nil? || xml.at_xpath('//me:encoding', me: XMLNS).nil? || xml.at_xpath('//me:alg', me: XMLNS).nil? | ||
|
||
data_element = xml.at_xpath('//me:data', me: XMLNS) | ||
sig_element = xml.at_xpath('//me:sig', me: XMLNS) | ||
encoding_element = xml.at_xpath('//me:encoding', me: XMLNS) | ||
alg_element = xml.at_xpath('//me:alg', me: XMLNS) | ||
|
||
super OStatus2::Salmon::decode_base64url(data_element.content.gsub(/\s+/, '')), | ||
OStatus2::Salmon::decode_base64url(sig_element.content.gsub(/\s+/, '')), | ||
sig_element.attribute('key_id')&.value, | ||
data_element.attribute('type').value, | ||
encoding_element.content, | ||
alg_element.content | ||
else | ||
args[3] ||= 'application/atom+xml' | ||
args[4] ||= 'base64url' | ||
args[5] ||= 'RSA-SHA256' | ||
|
||
if args[1].is_a?(OpenSSL::PKey::RSA) | ||
key = args[1] | ||
plaintext = plaintext_sig(args[0], args[3], args[4], args[5]) | ||
args[1] = key.sign(digest, plaintext) | ||
args[2] ||= OStatus2::Salmon.encode_base64url(key.public_key.to_s) | ||
end | ||
|
||
super | ||
end | ||
end | ||
|
||
# Serialize a magic envelope into XML | ||
# @return [String] Magic envelope XML serialization | ||
def to_xml | ||
Nokogiri::XML::Builder.new do |xml| | ||
xml['me'].env({ 'xmlns:me' => XMLNS }) do | ||
xml['me'].data({ type: type }, OStatus2::Salmon.encode_base64url(body)) | ||
xml['me'].encoding(encoding) | ||
xml['me'].alg(alg) | ||
xml['me'].sig({ key_id: key_id }, OStatus2::Salmon.encode_base64url(sig)) | ||
end | ||
end.to_xml | ||
end | ||
|
||
# Verify the magic envelope's integrity | ||
# @param [OpenSSL::PKey::RSA] key The public part of the key will be used | ||
# @return [Boolean] | ||
def verify(key) | ||
plaintext = plaintext_sig(body, type, encoding, alg) | ||
key.public_key.verify(digest, sig, plaintext) | ||
rescue BadError | ||
false | ||
end | ||
|
||
class << self | ||
# Represents an error due to malformed magic envelope | ||
class BadError < OStatus2::Error | ||
end | ||
|
||
# Deliver the magic envelope XML serialization to a Salmon endpoint | ||
# @param [String] salmon_url Salmon endpoint URL | ||
# @param [String] envelope Magic envelope XML serialization | ||
# @raise [HTTP::Error] Error raised upon delivery failure | ||
# @raise [OpenSSL::SSL::SSLError] Error raised upon SSL-related failure during delivery | ||
# @return [HTTP::Response] | ||
def post_xml(salmon_url, envelope) | ||
http_client.headers(HTTP::Headers::CONTENT_TYPE => 'application/magic-envelope+xml').post(Addressable::URI.parse(salmon_url), body: envelope) | ||
end | ||
|
||
private | ||
|
||
def http_client | ||
HTTP.timeout(:per_operation, write: 60, connect: 20, read: 60) | ||
end | ||
end | ||
|
||
private | ||
|
||
def digest | ||
OpenSSL::Digest::SHA256.new | ||
end | ||
|
||
def plaintext_sig(data, type, encoding, alg) | ||
[data, type, encoding, alg].map { |i| OStatus2::Salmon.encode_base64url(i) }.join('.') | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
module OStatus2 | ||
module Salmon | ||
# Represents public key | ||
MagicPublicKey = Struct.new(:n, :e) do | ||
# @!method initialize(n, e) | ||
# @param [Numeric, String] n Modulus or a magic public key or | ||
# public key in application/magic-key format as the only argument | ||
# @param [Numeric] e Exponent | ||
def initialize(*args) | ||
if args.size == 1 | ||
_, modulus, exponent = args[0].split('.') | ||
decoded = [modulus, exponent].map { |n| OStatus2::Salmon.decode_base64url(n).bytes.inject(0) { |a, e| (a << 8) | e } } | ||
super *decoded | ||
else | ||
super | ||
end | ||
end | ||
|
||
# Format itself into application/magic-key format | ||
# @return [String] Public key in application/magic-key format | ||
def format | ||
encoded = [n, e].map do |component| | ||
result = [] | ||
|
||
until component.zero? | ||
result << [component % 256].pack('C') | ||
component >>= 8 | ||
end | ||
|
||
OStatus2::Salmon.encode_base64url(result.reverse.join) | ||
end | ||
|
||
(['data:application/magic-public-key,RSA'] + encoded).join('.') | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# This is a backport of the following: | ||
# Document block for Struct.new if present by akihikodaki · Pull Request #1099 · lsegal/yard | ||
# https://github.com/lsegal/yard/pull/1099 | ||
class OStatus2YARDHandler < YARD::Handlers::Ruby::Base | ||
include YARD::Handlers::Ruby::StructHandlerMethods | ||
handles :assign | ||
|
||
process do | ||
if statement[1].call? && statement[1][0][0] == s(:const, 'Struct') && statement[1][2] == s(:ident, 'new') | ||
parse_block(statement[1].block[1], | ||
namespace: create_class(statement[0][0][0], P(:Struct))) | ||
end | ||
end | ||
end |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.