/*
* Copyright (c) 2018, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
// Package accesscontrol implements an access control authorization scheme
// based on digital signatures.
//
// Authorizations for specified access types are issued by an entity that
// digitally signs each authorization. The digital signature is verified
// by service providers before granting the specified access type. Each
// authorization includes an expiry date and a unique ID that may be used
// to mitigate malicious reuse/sharing of authorizations.
//
// In a typical deployment, the signing keys will be present on issuing
// entities which are distinct from service providers. Only verification
// keys will be deployed to service providers.
//
// An authorization is represented in JSON, which is then base64-encoded
// for transport:
//
// {
// "Authorization" : {
// "ID" : ,
// "AccessType" : ,
// "Expires" :
// },
// "SigningKeyID" : ,
// "Signature" :
// }
//
package accesscontrol
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"io"
"time"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
"golang.org/x/crypto/hkdf"
)
const (
keyIDLength = 32
authorizationIDKeyLength = 32
authorizationIDLength = 32
)
// SigningKey is the private key used to sign newly issued
// authorizations for the specified access type. The key ID
// is included in authorizations and identifies the
// corresponding verification keys.
//
// AuthorizationIDKey is used to produce a unique
// authentication ID that cannot be mapped back to its seed
// value.
type SigningKey struct {
ID []byte
AccessType string
AuthorizationIDKey []byte
PrivateKey []byte
}
// VerificationKey is the public key used to verify signed
// authentications issued for the specified access type. The
// authorization references the expected public key by ID.
type VerificationKey struct {
ID []byte
AccessType string
PublicKey []byte
}
// NewKeyPair generates a new authorization signing key pair.
func NewKeyPair(
accessType string) (*SigningKey, *VerificationKey, error) {
ID := make([]byte, keyIDLength)
_, err := rand.Read(ID)
if err != nil {
return nil, nil, errors.Trace(err)
}
authorizationIDKey := make([]byte, authorizationIDKeyLength)
_, err = rand.Read(authorizationIDKey)
if err != nil {
return nil, nil, errors.Trace(err)
}
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, errors.Trace(err)
}
signingKey := &SigningKey{
ID: ID,
AccessType: accessType,
AuthorizationIDKey: authorizationIDKey,
PrivateKey: privateKey,
}
verificationKey := &VerificationKey{
ID: ID,
AccessType: accessType,
PublicKey: publicKey,
}
return signingKey, verificationKey, nil
}
// Authorization describes an authorization, with a unique ID,
// granting access to a specified access type, and expiring at
// the specified time.
//
// An Authorization is embedded within a digitally signed
// object. This wrapping object adds a signature and a signing
// key ID.
type Authorization struct {
ID []byte
AccessType string
Expires time.Time
}
type signedAuthorization struct {
Authorization json.RawMessage
SigningKeyID []byte
Signature []byte
}
// ValidateSigningKey checks that a signing key is correctly configured.
func ValidateSigningKey(signingKey *SigningKey) error {
if len(signingKey.ID) != keyIDLength ||
len(signingKey.AccessType) < 1 ||
len(signingKey.AuthorizationIDKey) != authorizationIDKeyLength ||
len(signingKey.PrivateKey) != ed25519.PrivateKeySize {
return errors.TraceNew("invalid signing key")
}
return nil
}
// IssueAuthorization issues an authorization signed with the
// specified signing key.
//
// seedAuthorizationID should be a value that uniquely identifies
// the purchase, subscription, or transaction that backs the
// authorization; a distinct unique authorization ID will be derived
// from the seed without revealing the original value. The authorization
// ID is to be used to mitigate malicious authorization reuse/sharing.
//
// The first return value is a base64-encoded, serialized JSON representation
// of the signed authorization that can be passed to VerifyAuthorization. The
// second return value is the unique ID of the signed authorization returned in
// the first value.
func IssueAuthorization(
signingKey *SigningKey,
seedAuthorizationID []byte,
expires time.Time) (string, []byte, error) {
err := ValidateSigningKey(signingKey)
if err != nil {
return "", nil, errors.Trace(err)
}
hkdf := hkdf.New(sha256.New, signingKey.AuthorizationIDKey, nil, seedAuthorizationID)
ID := make([]byte, authorizationIDLength)
_, err = io.ReadFull(hkdf, ID)
if err != nil {
return "", nil, errors.Trace(err)
}
auth := Authorization{
ID: ID,
AccessType: signingKey.AccessType,
Expires: expires.UTC(),
}
authJSON, err := json.Marshal(auth)
if err != nil {
return "", nil, errors.Trace(err)
}
signature := ed25519.Sign(signingKey.PrivateKey, authJSON)
signedAuth := signedAuthorization{
Authorization: authJSON,
SigningKeyID: signingKey.ID,
Signature: signature,
}
signedAuthJSON, err := json.Marshal(signedAuth)
if err != nil {
return "", nil, errors.Trace(err)
}
encodedSignedAuth := base64.StdEncoding.EncodeToString(signedAuthJSON)
return encodedSignedAuth, ID, nil
}
// VerificationKeyRing is a set of verification keys to be deployed
// to a service provider for verifying access authorizations.
type VerificationKeyRing struct {
Keys []*VerificationKey
}
// ValidateVerificationKeyRing checks that a verification key ring is
// correctly configured.
func ValidateVerificationKeyRing(keyRing *VerificationKeyRing) error {
for _, key := range keyRing.Keys {
if len(key.ID) != keyIDLength ||
len(key.AccessType) < 1 ||
len(key.PublicKey) != ed25519.PublicKeySize {
return errors.TraceNew("invalid verification key")
}
}
return nil
}
// VerifyAuthorization verifies the signed authorization and, when
// verified, returns the embedded Authorization struct with the
// access control information.
//
// The key ID in the signed authorization is used to select the
// appropriate verification key from the key ring.
func VerifyAuthorization(
keyRing *VerificationKeyRing,
encodedSignedAuthorization string) (*Authorization, error) {
err := ValidateVerificationKeyRing(keyRing)
if err != nil {
return nil, errors.Trace(err)
}
signedAuthorizationJSON, err := base64.StdEncoding.DecodeString(
encodedSignedAuthorization)
if err != nil {
return nil, errors.Trace(err)
}
var signedAuth signedAuthorization
err = json.Unmarshal(signedAuthorizationJSON, &signedAuth)
if err != nil {
return nil, errors.Trace(err)
}
if len(signedAuth.SigningKeyID) != keyIDLength {
return nil, errors.TraceNew("invalid key ID length")
}
if len(signedAuth.Signature) != ed25519.SignatureSize {
return nil, errors.TraceNew("invalid signature length")
}
var verificationKey *VerificationKey
for _, key := range keyRing.Keys {
if subtle.ConstantTimeCompare(signedAuth.SigningKeyID, key.ID) == 1 {
verificationKey = key
}
}
if verificationKey == nil {
return nil, errors.TraceNew("invalid key ID")
}
if !ed25519.Verify(
verificationKey.PublicKey, signedAuth.Authorization, signedAuth.Signature) {
return nil, errors.TraceNew("invalid signature")
}
var auth Authorization
err = json.Unmarshal(signedAuth.Authorization, &auth)
if err != nil {
return nil, errors.Trace(err)
}
if len(auth.ID) == 0 {
return nil, errors.TraceNew("invalid authorization ID")
}
if auth.AccessType != verificationKey.AccessType {
return nil, errors.TraceNew("invalid access type")
}
if auth.Expires.IsZero() {
return nil, errors.TraceNew("invalid expiry")
}
if auth.Expires.Before(time.Now().UTC()) {
return nil, errors.TraceNew("expired authorization")
}
return &auth, nil
}